]> git.proxmox.com Git - mirror_ovs.git/blob - utilities/checkpatch.py
checkpatch: Print results while checking HEAD and stdin.
[mirror_ovs.git] / utilities / checkpatch.py
1 #!/usr/bin/env python
2 # Copyright (c) 2016, 2017 Red Hat, Inc.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at:
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 from __future__ import print_function
16
17 import email
18 import getopt
19 import os
20 import re
21 import sys
22
23 __errors = 0
24 __warnings = 0
25 print_file_name = None
26 checking_file = False
27 total_line = 0
28 colors = False
29
30
31 def get_color_end():
32 global colors
33 if colors:
34 return "\033[00m"
35 return ""
36
37
38 def get_red_begin():
39 global colors
40 if colors:
41 return "\033[91m"
42 return ""
43
44
45 def get_yellow_begin():
46 global colors
47 if colors:
48 return "\033[93m"
49 return ""
50
51
52 def print_error(message):
53 global __errors
54 print("%sERROR%s: %s" % (get_red_begin(), get_color_end(), message))
55
56 __errors = __errors + 1
57
58
59 def print_warning(message):
60 global __warnings
61 print("%sWARNING%s: %s" % (get_yellow_begin(), get_color_end(), message))
62
63 __warnings = __warnings + 1
64
65
66 def reset_counters():
67 global __errors, __warnings
68
69 __errors = 0
70 __warnings = 0
71
72
73 # These are keywords whose names are normally followed by a space and
74 # something in parentheses (usually an expression) then a left curly brace.
75 #
76 # 'do' almost qualifies but it's also used as "do { ... } while (...);".
77 __parenthesized_constructs = 'if|for|while|switch|[_A-Z]+FOR_EACH[_A-Z]*'
78
79 __regex_added_line = re.compile(r'^\+{1,2}[^\+][\w\W]*')
80 __regex_subtracted_line = re.compile(r'^\-{1,2}[^\-][\w\W]*')
81 __regex_leading_with_whitespace_at_all = re.compile(r'^\s+')
82 __regex_leading_with_spaces = re.compile(r'^ +[\S]+')
83 __regex_trailing_whitespace = re.compile(r'[^\S]+$')
84 __regex_single_line_feed = re.compile(r'^\f$')
85 __regex_for_if_missing_whitespace = re.compile(r' +(%s)[\(]'
86 % __parenthesized_constructs)
87 __regex_for_if_too_much_whitespace = re.compile(r' +(%s) +[\(]'
88 % __parenthesized_constructs)
89 __regex_for_if_parens_whitespace = \
90 re.compile(r' +(%s) \( +[\s\S]+\)' % __parenthesized_constructs)
91 __regex_is_for_if_single_line_bracket = \
92 re.compile(r'^ +(%s) \(.*\)' % __parenthesized_constructs)
93 __regex_ends_with_bracket = \
94 re.compile(r'[^\s]\) {(\s+/\*[\s\Sa-zA-Z0-9\.,\?\*/+-]*)?$')
95 __regex_ptr_declaration_missing_whitespace = re.compile(r'[a-zA-Z0-9]\*[^*]')
96 __regex_is_comment_line = re.compile(r'^\s*(/\*|\*\s)')
97
98 skip_leading_whitespace_check = False
99 skip_trailing_whitespace_check = False
100 skip_block_whitespace_check = False
101 skip_signoff_check = False
102
103 # Don't enforce character limit on files that include these characters in their
104 # name, as they may have legitimate reasons to have longer lines.
105 #
106 # Python isn't checked as flake8 performs these checks during build.
107 line_length_blacklist = ['.am', '.at', 'etc', '.in', '.m4', '.mk', '.patch',
108 '.py']
109
110 # Don't enforce a requirement that leading whitespace be all spaces on
111 # files that include these characters in their name, since these kinds
112 # of files need lines with leading tabs.
113 leading_whitespace_blacklist = ['.mk', '.am', '.at']
114
115
116 def is_subtracted_line(line):
117 """Returns TRUE if the line in question has been removed."""
118 return __regex_subtracted_line.search(line) is not None
119
120
121 def is_added_line(line):
122 """Returns TRUE if the line in question is an added line.
123 """
124 global checking_file
125 return __regex_added_line.search(line) is not None or checking_file
126
127
128 def added_line(line):
129 """Returns the line formatted properly by removing diff syntax"""
130 global checking_file
131 if not checking_file:
132 return line[1:]
133 return line
134
135
136 def leading_whitespace_is_spaces(line):
137 """Returns TRUE if the leading whitespace in added lines is spaces
138 """
139 if skip_leading_whitespace_check:
140 return True
141 if (__regex_leading_with_whitespace_at_all.search(line) is not None and
142 __regex_single_line_feed.search(line) is None):
143 return __regex_leading_with_spaces.search(line) is not None
144
145 return True
146
147
148 def trailing_whitespace_or_crlf(line):
149 """Returns TRUE if the trailing characters is whitespace
150 """
151 if skip_trailing_whitespace_check:
152 return False
153 return (__regex_trailing_whitespace.search(line) is not None and
154 __regex_single_line_feed.search(line) is None)
155
156
157 def if_and_for_whitespace_checks(line):
158 """Return TRUE if there is appropriate whitespace after if, for, while
159 """
160 if skip_block_whitespace_check:
161 return True
162 if (__regex_for_if_missing_whitespace.search(line) is not None or
163 __regex_for_if_too_much_whitespace.search(line) is not None or
164 __regex_for_if_parens_whitespace.search(line)):
165 return False
166 return True
167
168
169 def if_and_for_end_with_bracket_check(line):
170 """Return TRUE if there is not a bracket at the end of an if, for, while
171 block which fits on a single line ie: 'if (foo)'"""
172
173 def balanced_parens(line):
174 """This is a rather naive counter - it won't deal with quotes"""
175 balance = 0
176 for letter in line:
177 if letter == '(':
178 balance += 1
179 elif letter == ')':
180 balance -= 1
181 return balance == 0
182
183 if __regex_is_for_if_single_line_bracket.search(line) is not None:
184 if not balanced_parens(line):
185 return True
186 if __regex_ends_with_bracket.search(line) is None:
187 return False
188 return True
189
190
191 def pointer_whitespace_check(line):
192 """Return TRUE if there is no space between a pointer name and the
193 asterisk that denotes this is a apionter type, ie: 'struct foo*'"""
194 return __regex_ptr_declaration_missing_whitespace.search(line) is not None
195
196
197 def line_length_check(line):
198 """Return TRUE if the line length is too long"""
199 if len(line) > 79:
200 return True
201 return False
202
203
204 def is_comment_line(line):
205 """Returns TRUE if the current line is part of a block comment."""
206 return __regex_is_comment_line.match(line) is not None
207
208
209 checks = [
210 {'regex': None,
211 'match_name':
212 lambda x: not any([fmt in x for fmt in line_length_blacklist]),
213 'check': lambda x: line_length_check(x),
214 'print': lambda: print_warning("Line length is >79-characters long")},
215
216 {'regex': None,
217 'match_name':
218 lambda x: not any([fmt in x for fmt in leading_whitespace_blacklist]),
219 'check': lambda x: not leading_whitespace_is_spaces(x),
220 'print': lambda: print_warning("Line has non-spaces leading whitespace")},
221
222 {'regex': None, 'match_name': None,
223 'check': lambda x: trailing_whitespace_or_crlf(x),
224 'print': lambda: print_warning("Line has trailing whitespace")},
225
226 {'regex': '(.c|.h)(.in)?$', 'match_name': None,
227 'prereq': lambda x: not is_comment_line(x),
228 'check': lambda x: not if_and_for_whitespace_checks(x),
229 'print': lambda: print_error("Improper whitespace around control block")},
230
231 {'regex': '(.c|.h)(.in)?$', 'match_name': None,
232 'prereq': lambda x: not is_comment_line(x),
233 'check': lambda x: not if_and_for_end_with_bracket_check(x),
234 'print': lambda: print_error("Inappropriate bracing around statement")},
235
236 {'regex': '(.c|.h)(.in)?$', 'match_name': None,
237 'prereq': lambda x: not is_comment_line(x),
238 'check': lambda x: pointer_whitespace_check(x),
239 'print':
240 lambda: print_error("Inappropriate spacing in pointer declaration")}
241 ]
242
243
244 def regex_function_factory(func_name):
245 regex = re.compile(r'\b%s\([^)]*\)' % func_name)
246 return lambda x: regex.search(x) is not None
247
248
249 def regex_error_factory(description):
250 return lambda: print_error(description)
251
252
253 std_functions = [
254 ('malloc', 'Use xmalloc() in place of malloc()'),
255 ('calloc', 'Use xcalloc() in place of calloc()'),
256 ('realloc', 'Use xrealloc() in place of realloc()'),
257 ('strdup', 'Use xstrdup() in place of strdup()'),
258 ('asprintf', 'Use xasprintf() in place of asprintf()'),
259 ('vasprintf', 'Use xvasprintf() in place of vasprintf()'),
260 ('strcpy', 'Use ovs_strlcpy() in place of strcpy()'),
261 ('strlcpy', 'Use ovs_strlcpy() in place of strlcpy()'),
262 ('strncpy', 'Use ovs_strzcpy() in place of strncpy()'),
263 ('strerror', 'Use ovs_strerror() in place of strerror()'),
264 ('sleep', 'Use xsleep() in place of sleep()'),
265 ('abort', 'Use ovs_abort() in place of abort()'),
266 ('assert', 'Use ovs_assert() in place of assert()'),
267 ('error', 'Use ovs_error() in place of error()'),
268 ]
269 checks += [
270 {'regex': '(.c|.h)(.in)?$',
271 'match_name': None,
272 'prereq': lambda x: not is_comment_line(x),
273 'check': regex_function_factory(function_name),
274 'print': regex_error_factory(description)}
275 for (function_name, description) in std_functions]
276
277
278 def get_file_type_checks(filename):
279 """Returns the list of checks for a file based on matching the filename
280 against regex."""
281 global checks
282 checkList = []
283 for check in checks:
284 if check['regex'] is None and check['match_name'] is None:
285 checkList.append(check)
286 if check['regex'] is not None and \
287 re.compile(check['regex']).search(filename) is not None:
288 checkList.append(check)
289 elif check['match_name'] is not None and check['match_name'](filename):
290 checkList.append(check)
291 return checkList
292
293
294 def run_checks(current_file, line, lineno):
295 """Runs the various checks for the particular line. This will take
296 filename into account."""
297 global checking_file, total_line
298 print_line = False
299 for check in get_file_type_checks(current_file):
300 if 'prereq' in check and not check['prereq'](line):
301 continue
302 if check['check'](line):
303 check['print']()
304 print_line = True
305
306 if print_line:
307 if checking_file:
308 print("%s:%d:" % (current_file, lineno))
309 else:
310 print("#%d FILE: %s:%d:" % (total_line, current_file, lineno))
311 print("%s\n" % line)
312
313
314 def ovs_checkpatch_parse(text, filename):
315 global print_file_name, total_line, checking_file
316 lineno = 0
317 signatures = []
318 co_authors = []
319 parse = 0
320 current_file = filename if checking_file else ''
321 previous_file = ''
322 scissors = re.compile(r'^[\w]*---[\w]*')
323 hunks = re.compile('^(---|\+\+\+) (\S+)')
324 hunk_differences = re.compile(
325 r'^@@ ([0-9-+]+),([0-9-+]+) ([0-9-+]+),([0-9-+]+) @@')
326 is_signature = re.compile(r'((\s*Signed-off-by: )(.*))$',
327 re.I | re.M | re.S)
328 is_co_author = re.compile(r'(\s*(Co-authored-by: )(.*))$',
329 re.I | re.M | re.S)
330 is_gerrit_change_id = re.compile(r'(\s*(change-id: )(.*))$',
331 re.I | re.M | re.S)
332
333 reset_counters()
334
335 for line in text.split('\n'):
336 if current_file != previous_file:
337 previous_file = current_file
338
339 lineno = lineno + 1
340 total_line = total_line + 1
341 if len(line) <= 0:
342 continue
343
344 if checking_file:
345 parse = 2
346
347 if parse == 1:
348 match = hunks.match(line)
349 if match:
350 parse = parse + 1
351 current_file = match.group(2)[2:]
352 print_file_name = current_file
353 continue
354 elif parse == 0:
355 if scissors.match(line):
356 parse = parse + 1
357 if not skip_signoff_check:
358 if len(signatures) == 0:
359 print_error("No signatures found.")
360 elif len(signatures) != 1 + len(co_authors):
361 print_error("Too many signoffs; "
362 "are you missing Co-authored-by lines?")
363 if not set(co_authors) <= set(signatures):
364 print_error("Co-authored-by/Signed-off-by corruption")
365 elif is_signature.match(line):
366 m = is_signature.match(line)
367 signatures.append(m.group(3))
368 elif is_co_author.match(line):
369 m = is_co_author.match(line)
370 co_authors.append(m.group(3))
371 elif is_gerrit_change_id.match(line):
372 print_error(
373 "Remove Gerrit Change-Id's before submitting upstream.")
374 print("%d: %s\n" % (lineno, line))
375 elif parse == 2:
376 newfile = hunks.match(line)
377 if newfile:
378 current_file = newfile.group(2)[2:]
379 print_file_name = current_file
380 continue
381 reset_line_number = hunk_differences.match(line)
382 if reset_line_number:
383 lineno = int(reset_line_number.group(3))
384 if lineno < 0:
385 lineno = -1 * lineno
386 lineno -= 1
387 if is_subtracted_line(line):
388 lineno -= 1
389 if not is_added_line(line):
390 continue
391
392 cmp_line = added_line(line)
393
394 # Skip files which have /datapath in them, since they are
395 # linux or windows coding standards
396 if current_file.startswith('datapath'):
397 continue
398 if current_file.startswith('include/linux'):
399 continue
400 run_checks(current_file, cmp_line, lineno)
401 if __errors or __warnings:
402 return -1
403 return 0
404
405
406 def usage():
407 print("""\
408 Open vSwitch checkpatch.py
409 Checks a patch for trivial mistakes.
410 usage:
411 %s [options] [PATCH | -f SOURCE | -1 | -2 | ...]
412
413 Input options:
414 -f|--check-file Arguments are source files, not patches.
415 -1, -2, ... Check recent commits in this repo.
416
417 Check options:
418 -h|--help This help message
419 -b|--skip-block-whitespace Skips the if/while/for whitespace tests
420 -l|--skip-leading-whitespace Skips the leading whitespace test
421 -s|--skip-signoff-lines Tolerate missing Signed-off-by line
422 -t|--skip-trailing-whitespace Skips the trailing whitespace test"""
423 % sys.argv[0])
424
425
426 def ovs_checkpatch_print_result(result):
427 global __warnings, __errors, total_line
428 if result < 0:
429 print("Lines checked: %d, Warnings: %d, Errors: %d\n" %
430 (total_line, __warnings, __errors))
431 else:
432 print("Lines checked: %d, no obvious problems found\n" % (total_line))
433
434
435 def ovs_checkpatch_file(filename):
436 try:
437 mail = email.message_from_file(open(filename, 'r'))
438 except:
439 print_error("Unable to parse file '%s'. Is it a patch?" % filename)
440 return -1
441
442 for part in mail.walk():
443 if part.get_content_maintype() == 'multipart':
444 continue
445 result = ovs_checkpatch_parse(part.get_payload(decode=False), filename)
446 ovs_checkpatch_print_result(result)
447 return result
448
449
450 def partition(pred, iterable):
451 """Returns [[trues], [falses]], where [trues] is the items in
452 'iterable' that satisfy 'pred' and [falses] is all the rest."""
453 trues = []
454 falses = []
455 for item in iterable:
456 if pred(item):
457 trues.append(item)
458 else:
459 falses.append(item)
460 return trues, falses
461
462
463 if __name__ == '__main__':
464 try:
465 numeric_options, args = partition(lambda s: re.match('-[0-9]+$', s),
466 sys.argv[1:])
467 n_patches = int(numeric_options[-1][1:]) if numeric_options else 0
468
469 optlist, args = getopt.getopt(args, 'bhlstf',
470 ["check-file",
471 "help",
472 "skip-block-whitespace",
473 "skip-leading-whitespace",
474 "skip-signoff-lines",
475 "skip-trailing-whitespace"])
476 except:
477 print("Unknown option encountered. Please rerun with -h for help.")
478 sys.exit(-1)
479
480 for o, a in optlist:
481 if o in ("-h", "--help"):
482 usage()
483 sys.exit(0)
484 elif o in ("-b", "--skip-block-whitespace"):
485 skip_block_whitespace_check = True
486 elif o in ("-l", "--skip-leading-whitespace"):
487 skip_leading_whitespace_check = True
488 elif o in ("-s", "--skip-signoff-lines"):
489 skip_signoff_check = True
490 elif o in ("-t", "--skip-trailing-whitespace"):
491 skip_trailing_whitespace_check = True
492 elif o in ("-f", "--check-file"):
493 checking_file = True
494 else:
495 print("Unknown option '%s'" % o)
496 sys.exit(-1)
497
498 if sys.stdout.isatty():
499 colors = True
500
501 if n_patches:
502 status = 0
503 for i in reversed(range(0, n_patches)):
504 revision = 'HEAD~%d' % i
505 f = os.popen('git format-patch -1 --stdout %s' % revision, 'r')
506 patch = f.read()
507 f.close()
508
509 print('== Checking %s ==' % revision)
510 result = ovs_checkpatch_parse(patch, revision)
511 ovs_checkpatch_print_result(result)
512 if result:
513 status = -1
514 sys.exit(status)
515
516 try:
517 filename = args[0]
518 except:
519 if sys.stdin.isatty():
520 usage()
521 sys.exit(-1)
522 result = ovs_checkpatch_parse(sys.stdin.read(), '-')
523 ovs_checkpatch_print_result(result)
524 sys.exit(result)
525 sys.exit(ovs_checkpatch_file(filename))