1#!/usr/bin/python3
2
3# SPDX-License-Identifier: GPL-2.0-or-later
4#
5# Copyright (C) 2019-2021  Patryk Obara <patryk.obara@gmail.com>
6
7# pylint: disable=invalid-name
8# pylint: disable=missing-docstring
9
10"""
11Count all compiler warnings and print a summary.
12
13It returns success to the shell if the number or warnings encountered
14is less than or equal to the desired maximum warnings (default: 0).
15
16You can override the default limit with MAX_WARNINGS environment variable or
17using --max-warnings option (see the description of argument in --help for
18details).
19
20note: new compilers include additional flag -fdiagnostics-format=[text|json],
21which could be used instead of parsing using regex, but we want to preserve
22human-readable output in standard log.
23"""
24
25import argparse
26import os
27import re
28import sys
29
30# For recognizing warnings in GCC format in stderr:
31#
32GCC_WARN_PATTERN = re.compile(r'([^:]+):(\d+):\d+: warning: .* \[-W(.+?)\](.*)')
33#                                ~~~~~   ~~~  ~~~           ~~      ~~~    ~~
34#                                ↑       ↑    ↑             ↑       ↑      ↑
35#                                file    line column  message    type  extra
36
37# For recognizing warnings in MSVC format:
38#
39MSVC_WARN_PATTERN = re.compile(r'.+>([^\(]+)\((\d+),\d+\): warning ([^:]+): .*')
40#                                ~~  ~~~~~~    ~~~  ~~~             ~~~~~   ~~
41#                                ↑   ↑         ↑    ↑               ↑        ↑
42#                          project   file      line column       code  message
43
44# For removing color when GCC is invoked with -fdiagnostics-color=always
45#
46ANSI_COLOR_PATTERN = re.compile(r'\x1b\[[0-9;]*[mGKH]')
47
48# For recognizing warnings from usr/* or subprojects files
49USR_OR_SUBPROJECTS_PATTERN = re.compile(r'^/usr/.*|.*/subprojects/.*')
50
51class warning_summaries:
52
53    def __init__(self):
54        self.types = {}
55        self.files = {}
56        self.lines = set()
57
58    def count_type(self, name):
59        self.types[name] = self.types.get(name, 0) + 1
60
61    def count_file(self, name):
62        self.files[name] = self.files.get(name, 0) + 1
63
64    def list_all(self):
65        for line in sorted(self.lines):
66            print(line)
67        print()
68
69    def print_files(self):
70        print("Warnings grouped by file:\n")
71        print_summary(self.files)
72
73    def print_types(self):
74        print("Warnings grouped by type:\n")
75        print_summary(self.types)
76
77
78def remove_colors(line):
79    return re.sub(ANSI_COLOR_PATTERN, '', line)
80
81
82def count_warning(gcc_format, line_no, line, warnings):
83    line = remove_colors(line)
84
85    pattern = GCC_WARN_PATTERN if gcc_format else MSVC_WARN_PATTERN
86    match = pattern.match(line)
87    if not match:
88        return 0
89
90    # Ignore out-of-scope warnings from system and subprojects.
91    file = match.group(1)
92    if USR_OR_SUBPROJECTS_PATTERN.match(file):
93        return 0
94
95    # Some warnings (e.g. effc++) are reported multiple times, once
96    # for every usage; ignore duplicates.
97    line = line.strip()
98    if line in warnings.lines:
99        return 0
100    warnings.lines.add(line)
101
102    # wline = match.group(2)
103    wtype = match.group(3)
104
105    if pattern == GCC_WARN_PATTERN and match.group(4):
106        print('Log file is corrupted: extra characters in line',
107              line_no, file=sys.stderr)
108
109    _, fname = os.path.split(file)
110    warnings.count_type(wtype)
111    warnings.count_file(fname)
112    return 1
113
114
115def get_input_lines(name):
116    if name == '-':
117        return sys.stdin.readlines()
118    if not os.path.isfile(name):
119        print('{}: no such file.'.format(name))
120        sys.exit(2)
121    with open(name, 'r', encoding='utf-8') as logs:
122        return logs.readlines()
123
124
125def find_longest_name_length(names):
126    return max(len(x) for x in names)
127
128
129def print_summary(issues):
130    size = find_longest_name_length(issues.keys()) + 1
131    items = list(issues.items())
132    for name, count in sorted(items, key=lambda x: (x[1], x[0]), reverse=True):
133        print('  {text:{field_size}s}: {count}'.format(
134            text=name, count=count, field_size=size))
135    print()
136
137
138def parse_args():
139    parser = argparse.ArgumentParser(
140        formatter_class=argparse.RawTextHelpFormatter, description=__doc__)
141
142    parser.add_argument(
143        'logfile',
144        metavar='LOGFILE',
145        help=("Path to the logfile, or use - to read from stdin"))
146
147    max_warnings = int(os.getenv('MAX_WARNINGS', '0'))
148    parser.add_argument(
149        '-m', '--max-warnings',
150        type=int,
151        default=max_warnings,
152        help='Override the maximum number of warnings.\n'
153             'Use value -1 to disable the check.')
154
155    parser.add_argument(
156        '-f', '--files',
157        action='store_true',
158        help='Group warnings by filename.')
159
160    parser.add_argument(
161        '-l', '--list',
162        action='store_true',
163        help='Display sorted list of all warnings.')
164
165    parser.add_argument(
166        '--msvc',
167        action='store_true',
168        help='Look for warnings using MSVC format.')
169
170    return parser.parse_args()
171
172
173def main():
174    rcode = 0
175    total = 0
176    warnings = warning_summaries()
177    args = parse_args()
178    use_gcc_format = not args.msvc
179    line_no = 1
180    for line in get_input_lines(args.logfile):
181        total += count_warning(use_gcc_format, line_no, line, warnings)
182        line_no += 1
183    if args.list:
184        warnings.list_all()
185    if args.files and warnings.files:
186        warnings.print_files()
187    if warnings.types:
188        warnings.print_types()
189    print('Total: {} warnings'.format(total), end='')
190    if args.max_warnings >= 0:
191        print(' (out of {} allowed)\n'.format(args.max_warnings))
192        if total > args.max_warnings:
193            print('Error: upper limit of allowed warnings is', args.max_warnings)
194            rcode = 1
195    else:
196        print('\n')
197    return rcode
198
199if __name__ == '__main__':
200    sys.exit(main())
201