#!/usr/bin/env perl
# See copyright, etc in below POD section.
######################################################################

use warnings;
use Getopt::Long;
use IO::File;
use Pod::Usage;
use strict;
use vars qw($Debug $VERSION);

$VERSION = '3.881';

our %SuppressMap = (
    # New cpp check error          Can suppress with old error
    'nullPointerRedundantCheck' => 'nullPointer',
    );

#======================================================================
# main

our $Opt_Debug;

autoflush STDOUT 1;
autoflush STDERR 1;
Getopt::Long::config("no_auto_abbrev","pass_through");
our @Opt_Args = ("cppcheck", grep { $_ ne "--debug" } @ARGV);
if (! GetOptions(
          # Local options
          "debug!"		=> sub { $Debug = 9; },
          "help"                => \&usage,
          "version"             => sub { print "Version $VERSION\n"; system("cppcheck","--version"); exit(0); },
    )) {
    die "%Error: Bad usage, try 'cppcheck_filtered --help'\n";
}

process();

#----------------------------------------------------------------------

sub usage {
    print "Version $VERSION\n";
    pod2usage(-verbose=>2, -exitval=>0, -output=>\*STDOUT, -noperldoc=>1);
    exit(1);  # Unreachable
}

#######################################################################

sub process {
    my $cmd = join(' ', @Opt_Args);
    print "\t$cmd\n" if $Debug;
    my $fh = IO::File->new("$cmd 2>&1 |");
    $fh or die "%Error: '$cmd' failed: $!\n";
    my %uniq;
    my %errs;
    my $last_error = "";
    while (defined(my $line = $fh->getline())) {
        print ">>>$line" if $Debug;
        $line =~ s/^\s+//;
        $line =~ s/Checking usage of global functions\.+//;  # Sometimes tacked at end-of-line
        # General gunk
        next if $uniq{$line}++;
        next if $line =~ m!^<\?xml version!;
        next if $line =~ m!^<cppcheck!;
        next if $line =~ m!^<results!;
        next if $line =~ m!^</results>!;
        next if $line =~ m!^<errors!;
        next if $line =~ m!^</error>!;
        next if $line =~ m!^</errors>!;
        next if $line =~ m!^<error.*id="unmatchedSuppression"!;  # --suppress=unmatchedSuppression doesn't work
        next if $line =~ m!Cppcheck cannot find all the include files!; # An earlier id line is more specific
        next if $line =~ m!^Checking !;
        next if $line =~ m!^make.*Entering directory !;
        next if $line =~ m!^make.*Leaving directory !;
        next if $line =~ m!^\s+$!g;

        # Specific suppressions (see _suppress also)
        next if $line =~ m!id="unusedPrivateFunction" .*::debug!;  # Doesn't know UINFO will use it

        # Output
        if ($line =~ /^cppcheck --/) {
            print $line if $Debug;
        } elsif ($line =~ m!^\d+/\d+ files checked!) {
            print $line;
        } else {
            my $suppress;
            # --xml-format=1
            if ($line =~ /file="([^"]+)"\s+line="(\d+)"\s+id="([^"]+)"/) {
                my $file = $1; my $linenum = $2; my $id = $3;
                #$file = $1 if $line =~ /file0="([^"]+)"/;  # Still incorrect
                $suppress = 1 if _suppress($file,$linenum,$id);
            }
            # --xml-format=2
            if ($line =~ /<error id.*/) {
                $last_error = $line;
                chomp $last_error;
                $suppress = 1;
            }
            elsif ($line =~ /<location.* file="([^"]+)"\s+line="(\d+)"/) {
                my $file = $1; my $linenum = $2; my $id;
                #$file = $1 if $line =~ /file0="([^"]+)"/;  # Still incorrect
                if ($last_error =~ / id="([^"]+)"/) {
                    $id = $1;
                } else {
                    $id = "?";
                }
                $suppress = 1 if _suppress($file,$linenum,$id);
                $suppress = 1 if $file eq "*";
                if (!$suppress) {
                    chomp $line;
                    print "$file:$linenum: ".$last_error."\n";
                    $suppress = 1;
                }
            }
            if (!$suppress) {
                my $eline = "%Error: cppcheck: $line";
                print $eline;
                $errs{$eline}++;
            }
        }
    }
    if (scalar(keys %errs)) {
        #my $all = join('',sort(keys %errs));
        #$Self->error("Cppcheck errors:\n$all");
        #die "%Error: cppcheck_filtered found errors\n";
        exit(1);
    }
}

######################################################################

sub _suppress {
    my $filename = shift;
    my $linenum = shift;
    my $id = shift;
    #print "-Suppression search $filename $linenum $id\n" if $Self->{verbose};

    return undef if $filename eq "*";

    # Cleanup for e.g. ../V3AstNodes.h
    $filename = "src/$1" if $filename =~ m!^\.\./(.*)!;

    # Specific suppressions
    return 1 if $id eq "missingInclude" && $filename =~ m!systemc.h!;
    return 1 if $id eq "missingInclude" && $filename =~ m!svdpi.h!;
    return 1 if $id eq "unusedFunction" && $filename =~ m!verilated_dpi.cpp!;
    return 1 if $id eq "unusedFunction" && $filename =~ m!verilated_vpi.cpp!;
    return 1 if $id eq "unreachableCode" && $filename =~ /V3ParseBison.c/;
    return 1 if $id eq 'variableScope' && $filename =~ /fstapi.c/;

    my $fh = IO::File->new("<$filename");
    if (!$fh) {
        warn "%Warning: $! $filename,";
        return undef;
    }
    my $l = 0;
    while (defined(my $line = $fh->getline())) {
        ++$l;
        if ($l+1 == $linenum) {
            if ($line =~ /cppcheck-suppress((\s+\S+)+)/) {
                my $supids = $1;
                foreach my $supid (split /\s+/, $supids) {
                    if ($supid eq $id
                        || $supid eq ($SuppressMap{$id}||'')) {
                        return 1;
                    }
                }
                warn "%Warning: $filename: $l: Found suppress for ids='$supids', not expected id='$id'\n";
            }
        }
        if ($l == $linenum) {
            if (0 &&   # We now use VL_DANGLING instead of this rule
                $id eq "uselessAssignmentPtrArg"
                && $line =~ /(delete|Delete|Edit).*p *= *(NULL|nullptr);/) {
                # delete(nodep); nodep=NULL;   # This is ok, it's how we prevent later using nodep
                return 1;
            }
        }
    }
    return undef;
}

1;

#######################################################################
__END__

=pod

=head1 NAME

cppcheck_filtered - cppcheck wrapper with post-processing

=head1 SYNOPSIS

  cppcheck_filtered ...normal cpp check flags...


=head1 DESCRIPTION

Cppcheck_Filtered is a wrapper for cppcheck that filters out unnecessary
warnings related to Verilator.

=head1 ARGUMENTS

Most arguments are passed through to cppcheck

=over 4

=item --help

Displays this message and program version and exits.

=item --version

Print the version number and exit.

=back

=head1 DISTRIBUTION

This is part of the L<https://www.veripool.org/> free Verilog EDA software
tool suite.  The latest version is available from CPAN and from
L<https://www.veripool.org/>.

Copyright 2014-2020 by Wilson Snyder. This program is free software; you
can redistribute it and/or modify it under the terms of either the GNU
Lesser General Public License Version 3 or the Perl Artistic License Version 2.0.

SPDX-License-Identifier: LGPL-3.0-only OR Artistic-2.0

=head1 AUTHORS

Wilson Snyder <wsnyder@wsnyder.org>

=head1 SEE ALSO

C<cppcheck>

=cut

######################################################################
### Local Variables:
### compile-command: "./cppcheck_filtered --xml V3Width.cpp"
### End:
