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