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