#!/usr/bin/python3

# SPDX-License-Identifier: GPL-2.0-or-later
#
# Copyright (C) 2020-2021  Patryk Obara <patryk.obara@gmail.com>

# pylint: disable=invalid-name
# pylint: disable=missing-docstring

"""
Count known GCC/Clang sanitizer runtime issues and print a summary.

It returns success to the shell if the number or issues encountered
is less than or equal to the desired maximum issues (default: 0).

You can override the default limit with MAX_ISSUES environment variable or
using --max-issues option (see the description of argument in --help for
details).
"""

import argparse
import os
import re
import sys

SAN_WARN_PATTERN = re.compile(r"==\d+==WARNING: ([^:]+): (.*)")
#                                                ~~~~~    ~~
#                                                ↑        ↑
#                                        sanitizer        message

SAN_ERR_PATTERN = re.compile(r"==\d+==ERROR: ([^:]+): (.*)")
#                                             ~~~~~    ~~
#                                             ↑        ↑
#                                     sanitizer        message

RT_ERR_PATTERN = re.compile(r"([^:]+):(\d+):\d+: runtime error: .*")
#                              ~~~~~   ~~~                      ~~
#                              ↑       ↑                        ↑
#                              file    line                     message

MEMLEAK_PATTERN_1 = re.compile(r"Direct leak of \d+ byte\(s\) in .*")

MEMLEAK_PATTERN_2 = re.compile(r"Indirect leak of \d+ byte\(s\) in .*")


def match_line(line):
    match = SAN_WARN_PATTERN.match(line)
    if match:
        return match.group(2)

    match = SAN_ERR_PATTERN.match(line)
    if match:
        return match.group(2)

    match = RT_ERR_PATTERN.match(line)
    if match:
        return "runtime error"

    match = MEMLEAK_PATTERN_1.match(line)
    if match:
        return "direct leak"

    match = MEMLEAK_PATTERN_2.match(line)
    if match:
        return "indirect leak"

    return None


def count_issue(line, issue_types):
    issue = match_line(line)
    if issue:
        count = issue_types.get(issue) or 0
        issue_types[issue] = count + 1
        return 1
    return 0


def get_input_lines(name):
    if name == "-":
        return sys.stdin.readlines()
    if not os.path.isfile(name):
        print("{}: no such file.".format(name))
        sys.exit(2)
    with open(name, "r", encoding="utf-8") as logs:
        return logs.readlines()


def find_longest_name_length(names):
    return max(len(x) for x in names)


def print_summary(issues):
    size = find_longest_name_length(issues.keys()) + 1
    items = list(issues.items())
    for name, count in sorted(items, key=lambda x: (x[1], x[0]), reverse=True):
        print(
            "  {text:{field_size}s}: {count}".format(
                text=name, count=count, field_size=size
            )
        )
    print()


def parse_args():
    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawTextHelpFormatter, description=__doc__
    )

    parser.add_argument(
        "logfile",
        metavar="LOGFILE",
        help="Path to the logfile, or use - to read from stdin",
    )

    max_issues = int(os.getenv("MAX_ISSUES", "0"))
    parser.add_argument(
        "-m",
        "--max-issues",
        type=int,
        default=max_issues,
        help="Override the maximum number of issues.\n"
        "Use value -1 to disable the check.",
    )

    return parser.parse_args()


def main():
    rcode = 0
    total = 0
    issue_types = {}
    args = parse_args()
    for line in get_input_lines(args.logfile):
        total += count_issue(line, issue_types)
    if issue_types:
        print("Issues grouped by type:\n")
        print_summary(issue_types)
    print("Total: {} issues".format(total), end="")
    if args.max_issues >= 0:
        print(" (out of {} allowed)\n".format(args.max_issues))
        if total > args.max_issues:
            print("Error: upper limit of allowed issues is", args.max_issues)
            rcode = 1
    else:
        print("\n")
    return rcode


if __name__ == "__main__":
    sys.exit(main())
