1# SPDX-License-Identifier: GPL-2.0+ 2# Copyright (c) 2011 The Chromium OS Authors. 3# 4 5import collections 6import os 7import re 8import sys 9 10from patman import command 11from patman import gitutil 12from patman import terminal 13 14EMACS_PREFIX = r'(?:[0-9]{4}.*\.patch:[0-9]+: )?' 15TYPE_NAME = r'([A-Z_]+:)?' 16RE_ERROR = re.compile(r'ERROR:%s (.*)' % TYPE_NAME) 17RE_WARNING = re.compile(EMACS_PREFIX + r'WARNING:%s (.*)' % TYPE_NAME) 18RE_CHECK = re.compile(r'CHECK:%s (.*)' % TYPE_NAME) 19RE_FILE = re.compile(r'#(\d+): (FILE: ([^:]*):(\d+):)?') 20RE_NOTE = re.compile(r'NOTE: (.*)') 21 22 23def FindCheckPatch(): 24 top_level = gitutil.GetTopLevel() 25 try_list = [ 26 os.getcwd(), 27 os.path.join(os.getcwd(), '..', '..'), 28 os.path.join(top_level, 'tools'), 29 os.path.join(top_level, 'scripts'), 30 '%s/bin' % os.getenv('HOME'), 31 ] 32 # Look in current dir 33 for path in try_list: 34 fname = os.path.join(path, 'checkpatch.pl') 35 if os.path.isfile(fname): 36 return fname 37 38 # Look upwwards for a Chrome OS tree 39 while not os.path.ismount(path): 40 fname = os.path.join(path, 'src', 'third_party', 'kernel', 'files', 41 'scripts', 'checkpatch.pl') 42 if os.path.isfile(fname): 43 return fname 44 path = os.path.dirname(path) 45 46 sys.exit('Cannot find checkpatch.pl - please put it in your ' + 47 '~/bin directory or use --no-check') 48 49 50def CheckPatchParseOneMessage(message): 51 """Parse one checkpatch message 52 53 Args: 54 message: string to parse 55 56 Returns: 57 dict: 58 'type'; error or warning 59 'msg': text message 60 'file' : filename 61 'line': line number 62 """ 63 64 if RE_NOTE.match(message): 65 return {} 66 67 item = {} 68 69 err_match = RE_ERROR.match(message) 70 warn_match = RE_WARNING.match(message) 71 check_match = RE_CHECK.match(message) 72 if err_match: 73 item['cptype'] = err_match.group(1) 74 item['msg'] = err_match.group(2) 75 item['type'] = 'error' 76 elif warn_match: 77 item['cptype'] = warn_match.group(1) 78 item['msg'] = warn_match.group(2) 79 item['type'] = 'warning' 80 elif check_match: 81 item['cptype'] = check_match.group(1) 82 item['msg'] = check_match.group(2) 83 item['type'] = 'check' 84 else: 85 message_indent = ' ' 86 print('patman: failed to parse checkpatch message:\n%s' % 87 (message_indent + message.replace('\n', '\n' + message_indent)), 88 file=sys.stderr) 89 return {} 90 91 file_match = RE_FILE.search(message) 92 # some messages have no file, catch those here 93 no_file_match = any(s in message for s in [ 94 '\nSubject:', 'Missing Signed-off-by: line(s)', 95 'does MAINTAINERS need updating' 96 ]) 97 98 if file_match: 99 err_fname = file_match.group(3) 100 if err_fname: 101 item['file'] = err_fname 102 item['line'] = int(file_match.group(4)) 103 else: 104 item['file'] = '<patch>' 105 item['line'] = int(file_match.group(1)) 106 elif no_file_match: 107 item['file'] = '<patch>' 108 else: 109 message_indent = ' ' 110 print('patman: failed to find file / line information:\n%s' % 111 (message_indent + message.replace('\n', '\n' + message_indent)), 112 file=sys.stderr) 113 114 return item 115 116 117def CheckPatchParse(checkpatch_output, verbose=False): 118 """Parse checkpatch.pl output 119 120 Args: 121 checkpatch_output: string to parse 122 verbose: True to print out every line of the checkpatch output as it is 123 parsed 124 125 Returns: 126 namedtuple containing: 127 ok: False=failure, True=ok 128 problems: List of problems, each a dict: 129 'type'; error or warning 130 'msg': text message 131 'file' : filename 132 'line': line number 133 errors: Number of errors 134 warnings: Number of warnings 135 checks: Number of checks 136 lines: Number of lines 137 stdout: checkpatch_output 138 """ 139 fields = ['ok', 'problems', 'errors', 'warnings', 'checks', 'lines', 140 'stdout'] 141 result = collections.namedtuple('CheckPatchResult', fields) 142 result.stdout = checkpatch_output 143 result.ok = False 144 result.errors, result.warnings, result.checks = 0, 0, 0 145 result.lines = 0 146 result.problems = [] 147 148 # total: 0 errors, 0 warnings, 159 lines checked 149 # or: 150 # total: 0 errors, 2 warnings, 7 checks, 473 lines checked 151 emacs_stats = r'(?:[0-9]{4}.*\.patch )?' 152 re_stats = re.compile(emacs_stats + 153 r'total: (\d+) errors, (\d+) warnings, (\d+)') 154 re_stats_full = re.compile(emacs_stats + 155 r'total: (\d+) errors, (\d+) warnings, (\d+)' 156 r' checks, (\d+)') 157 re_ok = re.compile(r'.*has no obvious style problems') 158 re_bad = re.compile(r'.*has style problems, please review') 159 160 # A blank line indicates the end of a message 161 for message in result.stdout.split('\n\n'): 162 if verbose: 163 print(message) 164 165 # either find stats, the verdict, or delegate 166 match = re_stats_full.match(message) 167 if not match: 168 match = re_stats.match(message) 169 if match: 170 result.errors = int(match.group(1)) 171 result.warnings = int(match.group(2)) 172 if len(match.groups()) == 4: 173 result.checks = int(match.group(3)) 174 result.lines = int(match.group(4)) 175 else: 176 result.lines = int(match.group(3)) 177 elif re_ok.match(message): 178 result.ok = True 179 elif re_bad.match(message): 180 result.ok = False 181 else: 182 problem = CheckPatchParseOneMessage(message) 183 if problem: 184 result.problems.append(problem) 185 186 return result 187 188 189def CheckPatch(fname, verbose=False, show_types=False): 190 """Run checkpatch.pl on a file and parse the results. 191 192 Args: 193 fname: Filename to check 194 verbose: True to print out every line of the checkpatch output as it is 195 parsed 196 show_types: Tell checkpatch to show the type (number) of each message 197 198 Returns: 199 namedtuple containing: 200 ok: False=failure, True=ok 201 problems: List of problems, each a dict: 202 'type'; error or warning 203 'msg': text message 204 'file' : filename 205 'line': line number 206 errors: Number of errors 207 warnings: Number of warnings 208 checks: Number of checks 209 lines: Number of lines 210 stdout: Full output of checkpatch 211 """ 212 chk = FindCheckPatch() 213 args = [chk, '--no-tree'] 214 if show_types: 215 args.append('--show-types') 216 output = command.Output(*args, fname, raise_on_error=False) 217 218 return CheckPatchParse(output, verbose) 219 220 221def GetWarningMsg(col, msg_type, fname, line, msg): 222 '''Create a message for a given file/line 223 224 Args: 225 msg_type: Message type ('error' or 'warning') 226 fname: Filename which reports the problem 227 line: Line number where it was noticed 228 msg: Message to report 229 ''' 230 if msg_type == 'warning': 231 msg_type = col.Color(col.YELLOW, msg_type) 232 elif msg_type == 'error': 233 msg_type = col.Color(col.RED, msg_type) 234 elif msg_type == 'check': 235 msg_type = col.Color(col.MAGENTA, msg_type) 236 line_str = '' if line is None else '%d' % line 237 return '%s:%s: %s: %s\n' % (fname, line_str, msg_type, msg) 238 239def CheckPatches(verbose, args): 240 '''Run the checkpatch.pl script on each patch''' 241 error_count, warning_count, check_count = 0, 0, 0 242 col = terminal.Color() 243 244 for fname in args: 245 result = CheckPatch(fname, verbose) 246 if not result.ok: 247 error_count += result.errors 248 warning_count += result.warnings 249 check_count += result.checks 250 print('%d errors, %d warnings, %d checks for %s:' % (result.errors, 251 result.warnings, result.checks, col.Color(col.BLUE, fname))) 252 if (len(result.problems) != result.errors + result.warnings + 253 result.checks): 254 print("Internal error: some problems lost") 255 for item in result.problems: 256 sys.stderr.write( 257 GetWarningMsg(col, item.get('type', '<unknown>'), 258 item.get('file', '<unknown>'), 259 item.get('line', 0), item.get('msg', 'message'))) 260 print 261 #print(stdout) 262 if error_count or warning_count or check_count: 263 str = 'checkpatch.pl found %d error(s), %d warning(s), %d checks(s)' 264 color = col.GREEN 265 if warning_count: 266 color = col.YELLOW 267 if error_count: 268 color = col.RED 269 print(col.Color(color, str % (error_count, warning_count, check_count))) 270 return False 271 return True 272