#!/usr/bin/env python3
# pylint: disable=C0103,C0114,C0115,C0116,R0911,R0912,R0915,W0621
######################################################################

import argparse
import os
import re
import sys

SuppressMap = {
    # New cpp check error        Can suppress with old error
    'nullPointerRedundantCheck': 'nullPointer'
}

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


def process(cppcheck_args):
    cmd = " ".join(cppcheck_args) + " 2>&1"
    if Args.debug:
        print("\t" + cmd)
    fh = os.popen(cmd)
    uniq = {}
    errs = False
    last_error = ""
    for line in fh:
        line = line.rstrip()
        if Args.debug:
            print(">>>" + line)
        line = re.sub(r'^\s+', '', line)
        # Sometimes tacked at end-of-line
        line = re.sub(r'Checking usage of global functions\.+', '', line)
        # General gunk
        if line in uniq:
            continue
        uniq[line] = 1

        if re.search(r'^<\?xml version', line):
            continue
        if re.search(r'^<cppcheck', line):
            continue
        if re.search(r'^<results', line):
            continue
        if re.search(r'^</results>', line):
            continue
        if re.search(r'^<errors', line):
            continue
        if re.search(r'^</error>', line):
            continue
        if re.search(r'^</errors>', line):
            continue
        # --suppress=unmatchedSuppression doesn't work
        if re.search(r'^<error.*id="unmatchedSuppression"', line):
            continue
        # An earlier id line is more specific
        if re.search(r'Cppcheck cannot find all the include files', line):
            continue
        if re.search(r'^Checking ', line):
            continue
        if re.search(r'^make.*Entering directory ', line):
            continue
        if re.search(r'^make.*Leaving directory ', line):
            continue
        if re.search(r'^\s+$', line):
            continue

        # Specific suppressions (see _suppress also)
        # Doesn't know UINFO will use it
        if re.search(r'id="unusedPrivateFunction" .*::debug', line):
            continue

        # Output
        if re.search(r'^cppcheck --', line):
            if Args.debug:
                print(line)
            continue
        if re.search(r'^\d+/\d+ files checked', line):
            print(line)
            continue

        suppress = False
        # --xml-format=2
        if re.search(r'<error id', line):
            last_error = line
            continue

        match = re.search(r'<location.* file="([^"]+)"\s+line="(\d+)"', line)
        if match:
            file = match.group(1)
            linenum = match.group(2)
            match = re.search(r' id="([^"]+)"', last_error)
            eid = match.group(1) if match else '?'
            if _suppress(file, linenum, eid):
                suppress = True
            if file == "*":
                suppress = True
            if not suppress:
                print(file + ":" + linenum + ": " + last_error)
                suppress = True

        if not suppress:
            eline = "%Error: cppcheck: " + line
            print(eline)
            errs = True

    if errs:
        sys.exit(1)


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


def _suppress(filename, linenum, eid):
    if Args.debug:
        print("-Suppression search %s %s %s" % (filename, linenum, eid))

    if filename == "*":
        return False

    # Cleanup for e.g. ../V3AstNodes.h
    filename = re.sub(r'^\.\./(.*)', r'src/\1', filename)

    # Specific suppressions
    if eid == 'missingInclude' and re.search(r'systemc.h', filename):
        return True
    if eid == 'missingInclude' and re.search(r'svdpi.h', filename):
        return True
    if eid == 'unusedFunction' and re.search(r'verilated_dpi.cpp', filename):
        return True
    if eid == 'unusedFunction' and re.search(r'verilated_vpi.cpp', filename):
        return True
    if eid == 'unreachableCode' and re.search(r'V3ParseBison.c', filename):
        return True
    if eid == 'variableScope' and re.search(r'fstapi.c', filename):
        return True

    if not os.path.exists(filename):
        print("%Warning: " + filename + " does not exist, ignored",
              file=sys.stderr)
        return False

    with open(filename) as fh:
        lineno = 0
        for line in fh:
            lineno += 1
            if lineno + 1 == linenum:
                match = re.search(r'cppcheck-suppress((\s+\S+)+)', line)
                if match:
                    for supid in match.group(1).split():
                        if (supid == eid or (eid in SuppressMap
                                             and supid == SuppressMap[eid])):
                            return True
        return False


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

parser = argparse.ArgumentParser(
    allow_abbrev=False,
    formatter_class=argparse.RawDescriptionHelpFormatter,
    description="""cppcheck_filtered passes all arguments to cppcheck, then
filters out unnecessary warnings related to Verilator. Run as:

    cd $VERILATOR_ROOT
    make -k cppcheck""",
    epilog=
    """Copyright 2014-2022 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""")

parser.add_argument('--debug', action='store_true', help='enable debug')

Args, cppcheck_args = parser.parse_known_args()

process(cppcheck_args)

######################################################################
# Local Variables:
# compile-command: "cd .. ; src/cppcheck_filtered cppcheck --xml --enable=all src/V3Width.cpp"
# End:
