#!/usr/bin/perl -w

eval 'exec /usr/bin/perl -w -S $0 ${1+"$@"}'
    if 0; # not running under some shell
###############################################################################
# Sanity check plugin for the Krazy project.                                  #
# Copyright (C) 2006-2009 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.               #
#                                                                             #
###############################################################################

# Tests KDE source for proper include directives:
#  an installed header should never have an #include "..."
#  must #include <QtCore/QFoo> in an installed header.
#  check for multiple includes
#  check for include <QtModule>
#  check for includes that never should be used (blacklists)
#  foo.cpp always include your own foo.h first, then your private foo_p.h
#  no config.h in an installed header
#  config.h in angle brackets
# TODO: to include an installed header, always use #include <fred/ethel.h>
#       even if you are in library fred source
# TODO: an app "Foo" should include its own, non-installed headers with #include ".."
# TODO: check if foo.h is needed at all
# TODO: check if foo.h included in class.h should replaced with class Foo;

# Program options:
#   --help:          print one-line help message and exit
#   --version:       print one-line version information and exit
#   --priority:      report issues of the specified priority only
#   --strict:        report issues with the specified strictness level only
#   --explain:       print an explanation with solving instructions
#   --installed      file is to be installed
#   --quiet:         suppress all output messages
#   --verbose:       print the offending content

# Exits with status=0 if test condition is not present in the source;
# else exits with the number of failures encountered.

use strict;
use Tie::IxHash;
use File::Basename;
use Cwd 'abs_path';
use FindBin qw($Bin);
use lib "$Bin/../../../../lib";
use Krazy::PreProcess;
use Krazy::Utils;

my($Prog) = "includes";
my($Version) = "1.6";

&parseArgs();

&Help() if &helpArg();
&Version() if &versionArg();
&Explain() if &explainArg();
if ($#ARGV != 0){ &Help(); Exit 0; }

my($f) = $ARGV[0];
my($absf) = basename(abs_path($f));

# skip C source files
if ($f =~ m/\.c$/) {
  print "okay\n" if (!&quietArg());
  Exit 0;
}

#open file and slurp it in
open(F, "$f") || die "Couldn't open $f";
my(@data_lines) = <F>;
close(F);

# Remove C-style comments and #if 0 blocks from the file input
my(@lines) = RemoveIfZeroBlockC( RemoveCommentsC( @data_lines ) );

my($linecnt) = 0;
my($line);
my($total) = 0;  #total number of issues
# has of all issues we need to keep track of
tie my(%Issues), "Tie::IxHash";

my($incpath,$qincpath);
my($inc);
tie my(%Incs), "Tie::IxHash";

my($nth) = 1;    #track the include in the file
my($sep) = "";   #track the separator (angle bracket vs. double-quote)
my($guard) = ""; #include guard
my($sguard) = ""; #raw include guard

&initIssues();
while ($linecnt < $#lines) {
  $line = $lines[$linecnt++];
  if ($line =~ m+//.*[Kk]razy:excludeall=.*$Prog+ ||
      $line =~ m+//.*[Kk]razy:skip+) {
    print "okay\n" if (!&quietArg());
    Exit 0;
  }
  next if ($line =~ m+//.*[Kk]razy:exclude=.*$Prog+);
  $line =~ s+//.*++;  #skip C++ comments

  # get the include guard, if there is one
  if (!$guard &&
      ($line =~ m/^\s*#\s*ifndef\s/ ||
       $line =~ m/^\s*#\s*if\s*\!\s*defined\s*\(/ ||
       $line =~ m/^\s*#\s*if\s*\!\s*defined\s/)) {
    my($pguard) = $line;
    $pguard =~ s/^\s*#\s*ifndef\s//g;
    $pguard =~ s/^\s*#\s*if\s*\!\s*defined\s*\(//g;
    $pguard =~ s/^\s*#\s*if\s*\!\s*defined\s//g;
    $pguard =~ s/\s*\).*$//g;
    ($pguard) = split(' ',$pguard);
    $sguard = $pguard;
    $pguard =~ s/_[Hh]_*$//;
    $pguard =~ s/_//g;
    my($tguard) = $absf;
    $tguard =~ s/\.h.*$//;
    $tguard =~ s/\.hxx.*$//;
    $tguard =~ s/_//g;
    $tguard =~ s/-//g;
    $tguard =~ s/\.//g;
    $tguard =~ s/^k3//g;
    $tguard =~ s/\+\+/pp/;
    if ($tguard =~ m/$pguard/i || $pguard =~ m/$tguard/i) {
      $guard = $pguard;
    }
    next;
  }

  # get the include path, if there is one
  if ($line =~ m+^[[:space:]]*#include[[:space:]]+) {
    if ($line =~ m+\"+ ) {
      $sep = "\"";
      $incpath = &incPath($line);
    } else {
      $sep = "<";
      $incpath = &incAnglePath($line);
    }
    $qincpath = &qPath($incpath);
    if(!defined($Incs{$qincpath})) {
      if(!&configH($qincpath)){
	$Incs{$qincpath}{'nth'} = $nth++;       #cardinality, first instance
      }
      $Incs{$qincpath}{'sep'} = $sep;         #separator used, first instance
    }
    $Incs{$qincpath}{'count'}++;              #how many times we've seen it
    $Incs{$qincpath}{'lines'} .= "$linecnt,"; #and what linenos in the file
  } else {
    next;
  }

  # look for problems in installed headers
  if (&installedArg()) {
    # this file is an installed header

    # every file it includes must be in angle brackets
    if ($sep ne "<") {
      $Issues{'ANGLE'}{'count'}++;
      $Issues{'ANGLE'}{'lines'} .= "$linecnt,";
      print "=> $line\n" if (&verboseArg());
    }
    # every Qt file it includes must be of the form <QtModule/QFoo>
    if ($incpath =~ m+^(Q[A-Z]|Qt[A-Z]|Q3|q).*$+ && $incpath !~ m+qtest_kde+) {
      if ($incpath !~ m+^Qt.*/Q.*$+ && !&qModule($incpath) && !&qNotClasses($incpath)) {
        $Issues{'QINC'}{'count'}++;
        $Issues{'QINC'}{'lines'} .= "$linecnt,";
        print "=> $line\n" if (&verboseArg());
      }
    }
    if ($incpath =~ m+^K.*$+ && $incpath !~ m+^KDE/K.*$+ ) {
      $Issues{'KINC'}{'count'}++;
      $Issues{'KINC'}{'lines'} .= "$linecnt,";
      print "=> $line\n" if (&verboseArg());
    }
    # do not include config.h or config-foo.h in an installed header
    if (&configH($incpath)) {
      $Issues{'CONFIG'}{'count'}++;
      $Issues{'CONFIG'}{'lines'} .= "$linecnt,";
      print "=> $line\n" if (&verboseArg());
    }
    next;
  } else {
    # look for problems in non-installed files
    ### TODO
  }
}

### Now let's check for problems based solely on the include path
### independent of the type of file (installed or not).

foreach $inc (keys %Incs) {
  $Incs{$inc}{'lines'} =~ s/,$//; # remove trailing comma

  # check for multiply included files
  if ($Incs{$inc}{'count'} > 1) {
    $Issues{'DUPE'}{'count'}++;
    $Issues{'DUPE'}{'lines'} .= "$Incs{$inc}{'lines'}($inc);";
    print "=> $line\n" if (&verboseArg());
  }

  # check for including QtModule
  if (&qModule($inc)) {
    $Issues{'QMOD'}{'count'}++;
    $Issues{'QMOD'}{'lines'} .= "$Incs{$inc}{'lines'}($inc);";
    print "=> $line\n" if (&verboseArg());
  }

  # check for Qt/qfoo.h
  if ($inc =~ m+^Qt/q.*\.h+) {
    $Issues{'QMIX'}{'count'}++;
    $Issues{'QMIX'}{'lines'} .= "$Incs{$inc}{'lines'}($inc);";
    print "=> $line\n" if (&verboseArg());
  }

  # check for config.h in doublequotes
  if (&configH($inc) && $Incs{$inc}{'sep'} ne "<") {
    $Issues{'CONFIGSEP'}{'count'}++;
    $Issues{'CONFIGSEP'}{'lines'} .= "$Incs{$inc}{'lines'};";
    print "=> $line\n" if (&verboseArg());
  }

  # check for including a blacklisted include
  if (&blackList($inc)) {
    $Issues{'DONT'}{'count'}++;
    $Issues{'DONT'}{'lines'} .= "$Incs{$inc}{'lines'}($inc);";
    print "=> $line\n" if (&verboseArg());
  }
}

# check for include positions within a .cpp file
my($foo,$foop);
if ($f =~ m/\.cpp$/ || $f =~ m/\.cxx$/ || $f =~ m/\.cc$/) {
  $foo = basename(abs_path($f));
  $foo=~ s/\.cpp$//; $foo =~ s/\.cxx$//; $foo =~ s/\.cc$//;
  $foop = $foo . "_p.h";
  $foo  = $foo . ".h";
  if (defined($Incs{$foo})) {
    if ($Incs{$foo}{'nth'} != 1) {
      $Issues{'OWN1'}{'count'}++;
      $Issues{'OWN1'}{'lines'} .= "$Incs{$foo}{'lines'};";
      print "=> $lines[$Incs{$foo}{'lines'}-1]\n" if (&verboseArg());
    }
    if (defined($Incs{$foop})) {
      if ($Incs{$foop}{'nth'} != 2) {
        $Issues{'PRIV2'}{'count'}++;
        $Issues{'PRIV2'}{'lines'} .= "$Incs{$foop}{'lines'};";
        print "=> $lines[$Incs{$foop}{'lines'}-1]n" if (&verboseArg());
      }
    }
  } elsif (defined($Incs{$foop})) {
    if ($Incs{$foop}{'nth'} != 1) {
      $Issues{'PRIV1'}{'count'}++;
      $Issues{'PRIV1'}{'lines'} .= "$Incs{$foop}{'lines'};";
      print "=> $lines[$Incs{$foop}{'lines'}-1]\n" if (&verboseArg());
    }
  }

  $foo = basename(dirname(abs_path($f))) . "/" . $foo;
  $foop = basename(dirname(abs_path($f))) . "/" . $foop;
  if (defined($Incs{$foo})) {
    if ($Incs{$foo}{'nth'} != 1) {
      $Issues{'OWN1'}{'count'}++;
      $Issues{'OWN1'}{'lines'} .= "$Incs{$foo}{'lines'};";
      print "=> $lines[$Incs{$foo}{'lines'}-1]\n" if (&verboseArg());
    }
    if (defined($Incs{$foop})) {
      if ($Incs{$foop}{'nth'} != 2) {
        $Issues{'PRIV2'}{'count'}++;
        $Issues{'PRIV2'}{'lines'} .= "$Incs{$foop}{'lines'};";
        print "=> $lines[$Incs{$foop}{'lines'}-1]\n" if (&verboseArg());
      }
    }
  } elsif (defined($Incs{$foop})) {
    if ($Incs{$foop}{'nth'} != 1) {
      $Issues{'PRIV1'}{'count'}++;
      $Issues{'PRIV1'}{'lines'} .= "$Incs{$foop}{'lines'};";
      print "=> $lines[$Incs{$foop}{'lines'}-1]\n" if (&verboseArg());
    }
  }
}

# check for missing include guard (headers only)
if ($f =~ m/\.h$/ || $f =~ m/\.hxx$/ && $f !~ m+/tests/+) {
  if ($guard eq "") {
    $Issues{'GUARD'}{'count'} = 1;
  }
  if (&strictArg() eq "all") {
    if ($sguard =~ m/^_/ || $sguard =~ m/_$/){
      $Issues{'UGUARD'}{'count'} = 1;
    }
  }
}

my($cnt) = &printResults();
if (!$cnt) {
  print "okay\n" if (!&quietArg());
  Exit 0;
} else {
  Exit $cnt;
}

sub Help {
  print "Check for proper include directives\n";
  Exit 0 if &helpArg();
}

sub Version {
  print "$Prog, version $Version\n";
  Exit 0 if &versionArg();
}

sub Explain {
  print "Use <..> to include installed headers; <QtModule/QClass> to include Qt headers from installed headers; cpp file should include their own headers first (but below config.h); other rules apply, see <http://techbase.kde.org/Policies/Library_Code_Policy#Getting_.23includes_right>. Use include guards in headers with appropriatedly encoded macro names.";
  Exit 0 if &explainArg();
}

sub initIssues() {
  $Issues{'GUARD'}{'issue'} = "missing or improper include guard in header";
  $Issues{'GUARD'}{'explain'} = "Use an include guard in headers to ensure the header is not included more than once";
  $Issues{'GUARD'}{'count'} = 0;
  $Issues{'GUARD'}{'lines'} = '';

  $Issues{'UGUARD'}{'issue'} = "using leading or trailing underscores on include guard in header";
  $Issues{'UGUARD'}{'explain'} = "Do not use leading or trailing underscores on the include guard macro as as they are reserved for compiler/libc use.";
  $Issues{'UGUARD'}{'count'} = 0;
  $Issues{'UGUARD'}{'lines'} = '';

  $Issues{'ANGLE'}{'issue'} = "use angle brackets";
  $Issues{'ANGLE'}{'explain'} = "Use angle brackets to include installed headers or if the file is an installed header";
  $Issues{'ANGLE'}{'count'} = 0;
  $Issues{'ANGLE'}{'lines'} = '';

  $Issues{'QINC'}{'issue'} = "use QtModule/QClass in angle brackets";
  $Issues{'QINC'}{'explain'} = "Installed headers must include Qt headers using the form <QtModule/QClass>";
  $Issues{'QINC'}{'count'} = 0;
  $Issues{'QINC'}{'lines'} = '';

  $Issues{'KINC'}{'issue'} = "use KDE/KClass in angle brackets";
  $Issues{'KINC'}{'explain'} = "Installed headers must include KClass forwarding headers ithe form <KDE/KClass>";
  $Issues{'KINC'}{'count'} = 0;
  $Issues{'KINC'}{'lines'} = '';

  $Issues{'QMOD'}{'issue'} = "do not include QtModules";
  $Issues{'QMOD'}{'explain'} = "Do not include QtModules";
  $Issues{'QMOD'}{'count'} = 0;
  $Issues{'QMOD'}{'lines'} = '';

  $Issues{'QMIX'}{'issue'} = "do not include Qt/qfoo.h";
  $Issues{'QMIX'}{'explain'} = "Do not include Qt/qfoo.h. When compiling Mac OS X with frameworks enabled, there is no include/Qt/ directory. This directory only exists so that the qt3-style #include <qfoo.h> still works, but was never intended to be used as #include <Qt/foo.h>.";
  $Issues{'QMIX'}{'count'} = 0;
  $Issues{'QMIX'}{'lines'} = '';

  $Issues{'DUPE'}{'issue'} = "duplicate includes";
  $Issues{'DUPE'}{'explain'} = "Do not include a header more than once";
  $Issues{'DUPE'}{'count'} = 0;
  $Issues{'DUPE'}{'lines'} = '';

  $Issues{'DONT'}{'issue'} = "never include";
  $Issues{'DONT'}{'explain'} = "We never want to include these headers";
  $Issues{'DONT'}{'count'} = 0;
  $Issues{'DONT'}{'lines'} = '';

  $Issues{'OWN1'}{'issue'} = "include own header first";
  $Issues{'OWN1'}{'explain'} = "foo.cpp should always include its own header foo.h first, helping to check that foo.h is useable as standalone";
  $Issues{'OWN1'}{'count'} = 0;
  $Issues{'OWN1'}{'lines'} = '';

  $Issues{'PRIV2'}{'issue'} = "include own _p header second";
  $Issues{'PRIV2'}{'explain'} = "foo.cpp should include its own header foo.h first, followed by its private header foo_p.h second";
  $Issues{'PRIV2'}{'count'} = 0;
  $Issues{'PRIV2'}{'lines'} = '';

  $Issues{'PRIV1'}{'issue'} = "include own _p header first";
  $Issues{'PRIV1'}{'explain'} = "foo.cpp should include its own private header foo_p.h first if there is no foo.h header";
  $Issues{'PRIV1'}{'count'} = 0;
  $Issues{'PRIV1'}{'lines'} = '';

  $Issues{'CONFIG'}{'issue'} = "config.h in installed header";
  $Issues{'CONFIG'}{'explain'} = "config.h should not be included in an installed header";
  $Issues{'CONFIG'}{'count'} = 0;
  $Issues{'CONFIG'}{'lines'} = '';

  $Issues{'CONFIGSEP'}{'issue'} = "put config.h in angle brackets";
  $Issues{'CONFIGSEP'}{'explain'} = "Use angle brackets to include generated config.h headers";
  $Issues{'CONFIGSEP'}{'count'} = 0;
  $Issues{'CONFIGSEP'}{'lines'} = '';
}

sub printResults() {
  my($guy);
  my($check_num)=0;
  my($tot)=0;
  my($cline,$rline);
  my($fred);
  foreach $guy (keys %Issues) {
next if ($guy eq "ANGLE");  # controversial
    $cline = "$Issues{$guy}{'issue'}";

    $Issues{$guy}{'lines'} =~ s/,[[:space:]]*$//;
    $Issues{$guy}{'lines'} =~ s/;[[:space:]]*$//;
    if ($Issues{$guy}{'count'}) {
      $tot += $Issues{$guy}{'count'};
      print "$cline";
      print " line\#$Issues{$guy}{'lines'}" if ($Issues{$guy}{'lines'});
      print "\n";
    }
  }
  return $tot;
}

sub incPath {
  my($in) = @_;
  my($fred);
  $in =~ s+^\s*#\s*include[[:space:]]++;
  ($fred,$in) = split('"',$in);
  return $in;
}

sub incAnglePath {
  my($in) = @_;
  $in =~ s+^\s*#\s*include[[:space:]]++;
  $in =~ s+<++; $in =~ s+>++;
  return $in;
}

# turns QtModule/QFoo or QtModule/qfoo.h into qfoo.h
sub qPath {
  my($p) = @_;
  my($t);
  if ($p =~ m+^Qt[[:alpha:]]/[Qq].*$+) {
    ($t,$p) = split("/",$p);
    $p = lc($p);
    $p .= ".h" if ($p !~ m/\.h$/);
  }
  return $p;
}

# determine if $1 is a QtModule
sub qModule {
  my($p) = @_;
  if($p eq "QtCore" ||
     $p eq "QtGui" ||
     $p eq "QtNetwork" ||
     $p eq "QtOpenGL" ||
     $p eq "QtSql" ||
     $p eq "QtSvg" ||
     $p eq "QtXml" ||
     $p eq "QtDesigner" ||
     $p eq "QtUiTools" ||
     $p eq "QtAssistant" ||
     $p eq "Qt3Support" ||
     $p eq "QtTest") {
    return 1;
  } else {
    return 0
  }
}

# determine if $1 is not an exported class and therefore does
# not have an auto-generated <QtModule/QClass> file.
sub qNotClasses{
  my($p) = @_;
  if($p =~ m+[Qq]plugin+ || $p =~ m+(qgpgme|quota)+) {
    return 1;
  } else {
    return 0;
  }
}

# determine if $1 is a config.h type file
sub configH {
  my($p) = @_;
  if($p eq "config.h" ||
     $p =~ m+config-[[:alpha:]]*\.h+ ||
     $p =~ m+version-[[:alpha:]]*\.h+) {
    return 1;
  } else {
    return 0;
  }
}

# determine if $1 is a "blacklisted" include (never use it)
sub blackList {
  my($p) = @_;
#  if ($p =~ m+X11/+) {
#    return 1;
#  }
# apparently malloc.h is required on older systems that we support
#  if($p eq "malloc.h") {
#    return 1;
#  }
  return 0;
}
