checkpatch.py 20.2 KB
Newer Older
1
#!/usr/bin/env python
2
# Copyright (c) 2016, 2017 Red Hat, Inc.
3
# Copyright (c) 2018 Nicira, Inc.
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import print_function

import email
import getopt
20
import os
21 22 23 24 25
import re
import sys

__errors = 0
__warnings = 0
26
print_file_name = None
27
checking_file = False
28 29
total_line = 0
colors = False
30 31


32 33 34 35 36
def get_color_end():
    global colors
    if colors:
        return "\033[00m"
    return ""
37 38


39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
def get_red_begin():
    global colors
    if colors:
        return "\033[91m"
    return ""


def get_yellow_begin():
    global colors
    if colors:
        return "\033[93m"
    return ""


def print_error(message):
54
    global __errors
55
    print("%sERROR%s: %s" % (get_red_begin(), get_color_end(), message))
56 57 58 59

    __errors = __errors + 1


60
def print_warning(message):
61
    global __warnings
62
    print("%sWARNING%s: %s" % (get_yellow_begin(), get_color_end(), message))
63 64 65 66

    __warnings = __warnings + 1


67
def reset_counters():
68
    global __errors, __warnings, total_line
69 70 71

    __errors = 0
    __warnings = 0
72
    total_line = 0
73 74


75 76 77 78 79 80
# These are keywords whose names are normally followed by a space and
# something in parentheses (usually an expression) then a left curly brace.
#
# 'do' almost qualifies but it's also used as "do { ... } while (...);".
__parenthesized_constructs = 'if|for|while|switch|[_A-Z]+FOR_EACH[_A-Z]*'

81
__regex_added_line = re.compile(r'^\+{1,2}[^\+][\w\W]*')
82
__regex_subtracted_line = re.compile(r'^\-{1,2}[^\-][\w\W]*')
83 84 85
__regex_leading_with_whitespace_at_all = re.compile(r'^\s+')
__regex_leading_with_spaces = re.compile(r'^ +[\S]+')
__regex_trailing_whitespace = re.compile(r'[^\S]+$')
86
__regex_single_line_feed = re.compile(r'^\f$')
87 88 89 90
__regex_for_if_missing_whitespace = re.compile(r' +(%s)[\(]'
                                               % __parenthesized_constructs)
__regex_for_if_too_much_whitespace = re.compile(r' +(%s)  +[\(]'
                                                % __parenthesized_constructs)
91
__regex_for_if_parens_whitespace = \
92
    re.compile(r' +(%s) \( +[\s\S]+\)' % __parenthesized_constructs)
93
__regex_is_for_if_single_line_bracket = \
94
    re.compile(r'^ +(%s) \(.*\)' % __parenthesized_constructs)
95 96
__regex_ends_with_bracket = \
    re.compile(r'[^\s]\) {(\s+/\*[\s\Sa-zA-Z0-9\.,\?\*/+-]*)?$')
97
__regex_ptr_declaration_missing_whitespace = re.compile(r'[a-zA-Z0-9]\*[^*]')
98
__regex_is_comment_line = re.compile(r'^\s*(/\*|\*\s)')
99
__regex_has_comment = re.compile(r'.*(/\*|\*\s)')
100
__regex_trailing_operator = re.compile(r'^[^ ]* [^ ]*[?:]$')
101 102
__regex_conditional_else_bracing = re.compile(r'^\s*else\s*{?$')
__regex_conditional_else_bracing2 = re.compile(r'^\s*}\selse\s*$')
103
__regex_has_xxx_mark = re.compile(r'.*xxx.*', re.IGNORECASE)
104 105 106 107 108 109

skip_leading_whitespace_check = False
skip_trailing_whitespace_check = False
skip_block_whitespace_check = False
skip_signoff_check = False

110 111 112 113 114 115 116
# Don't enforce character limit on files that include these characters in their
# name, as they may have legitimate reasons to have longer lines.
#
# Python isn't checked as flake8 performs these checks during build.
line_length_blacklist = ['.am', '.at', 'etc', '.in', '.m4', '.mk', '.patch',
                         '.py']

117 118 119 120 121
# Don't enforce a requirement that leading whitespace be all spaces on
# files that include these characters in their name, since these kinds
# of files need lines with leading tabs.
leading_whitespace_blacklist = ['.mk', '.am', '.at']

122

123 124 125 126
def is_subtracted_line(line):
    """Returns TRUE if the line in question has been removed."""
    return __regex_subtracted_line.search(line) is not None

127

128 129 130
def is_added_line(line):
    """Returns TRUE if the line in question is an added line.
    """
131 132 133 134 135 136 137 138 139 140
    global checking_file
    return __regex_added_line.search(line) is not None or checking_file


def added_line(line):
    """Returns the line formatted properly by removing diff syntax"""
    global checking_file
    if not checking_file:
        return line[1:]
    return line
141 142 143 144 145 146 147


def leading_whitespace_is_spaces(line):
    """Returns TRUE if the leading whitespace in added lines is spaces
    """
    if skip_leading_whitespace_check:
        return True
148 149
    if (__regex_leading_with_whitespace_at_all.search(line) is not None and
            __regex_single_line_feed.search(line) is None):
150
        return __regex_leading_with_spaces.search(line) is not None
151

152 153 154 155 156 157 158 159
    return True


def trailing_whitespace_or_crlf(line):
    """Returns TRUE if the trailing characters is whitespace
    """
    if skip_trailing_whitespace_check:
        return False
160 161
    return (__regex_trailing_whitespace.search(line) is not None and
            __regex_single_line_feed.search(line) is None)
162 163 164 165 166 167 168 169 170 171 172 173 174 175


def if_and_for_whitespace_checks(line):
    """Return TRUE if there is appropriate whitespace after if, for, while
    """
    if skip_block_whitespace_check:
        return True
    if (__regex_for_if_missing_whitespace.search(line) is not None or
            __regex_for_if_too_much_whitespace.search(line) is not None or
            __regex_for_if_parens_whitespace.search(line)):
        return False
    return True


176 177 178 179 180 181 182 183
def if_and_for_end_with_bracket_check(line):
    """Return TRUE if there is not a bracket at the end of an if, for, while
       block which fits on a single line ie: 'if (foo)'"""

    def balanced_parens(line):
        """This is a rather naive counter - it won't deal with quotes"""
        balance = 0
        for letter in line:
184
            if letter == '(':
185
                balance += 1
186
            elif letter == ')':
187
                balance -= 1
188
        return balance == 0
189 190 191 192 193 194

    if __regex_is_for_if_single_line_bracket.search(line) is not None:
        if not balanced_parens(line):
            return True
        if __regex_ends_with_bracket.search(line) is None:
            return False
195 196 197 198
    if __regex_conditional_else_bracing.match(line) is not None:
        return False
    if __regex_conditional_else_bracing2.match(line) is not None:
        return False
199 200 201
    return True


202 203 204 205 206 207
def pointer_whitespace_check(line):
    """Return TRUE if there is no space between a pointer name and the
       asterisk that denotes this is a apionter type, ie: 'struct foo*'"""
    return __regex_ptr_declaration_missing_whitespace.search(line) is not None


208 209 210 211 212 213 214
def line_length_check(line):
    """Return TRUE if the line length is too long"""
    if len(line) > 79:
        return True
    return False


215 216 217 218
def is_comment_line(line):
    """Returns TRUE if the current line is part of a block comment."""
    return __regex_is_comment_line.match(line) is not None

219

220 221 222 223
def has_comment(line):
    """Returns TRUE if the current line contains a comment or is part of
       a block comment."""
    return __regex_has_comment.match(line) is not None
224

225

226 227 228 229
def trailing_operator(line):
    """Returns TRUE if the current line ends with an operatorsuch as ? or :"""
    return __regex_trailing_operator.match(line) is not None

230

231 232 233 234
def has_xxx_mark(line):
    """Returns TRUE if the current line contains 'xxx'."""
    return __regex_has_xxx_mark.match(line) is not None

235

236 237 238 239 240
checks = [
    {'regex': None,
     'match_name':
     lambda x: not any([fmt in x for fmt in line_length_blacklist]),
     'check': lambda x: line_length_check(x),
241
     'print': lambda: print_warning("Line length is >79-characters long")},
242

243 244 245
    {'regex': None,
     'match_name':
     lambda x: not any([fmt in x for fmt in leading_whitespace_blacklist]),
246
     'check': lambda x: not leading_whitespace_is_spaces(x),
247
     'print': lambda: print_warning("Line has non-spaces leading whitespace")},
248 249 250

    {'regex': None, 'match_name': None,
     'check': lambda x: trailing_whitespace_or_crlf(x),
251
     'print': lambda: print_warning("Line has trailing whitespace")},
252

253
    {'regex': '(\.c|\.h)(\.in)?$', 'match_name': None,
254
     'prereq': lambda x: not is_comment_line(x),
255
     'check': lambda x: not if_and_for_whitespace_checks(x),
256
     'print': lambda: print_error("Improper whitespace around control block")},
257

258
    {'regex': '(\.c|\.h)(\.in)?$', 'match_name': None,
259
     'prereq': lambda x: not is_comment_line(x),
260
     'check': lambda x: not if_and_for_end_with_bracket_check(x),
261
     'print': lambda: print_error("Inappropriate bracing around statement")},
262

263
    {'regex': '(\.c|\.h)(\.in)?$', 'match_name': None,
264
     'prereq': lambda x: not is_comment_line(x),
265 266
     'check': lambda x: pointer_whitespace_check(x),
     'print':
267 268 269 270 271 272 273
     lambda: print_error("Inappropriate spacing in pointer declaration")},

    {'regex': '(\.c|\.h)(\.in)?$', 'match_name': None,
     'prereq': lambda x: not is_comment_line(x),
     'check': lambda x: trailing_operator(x),
     'print':
     lambda: print_error("Line has '?' or ':' operator at end of line")},
274 275 276 277 278

    {'regex': '(\.c|\.h)(\.in)?$', 'match_name': None,
     'prereq': lambda x: has_comment(x),
     'check': lambda x: has_xxx_mark(x),
     'print': lambda: print_warning("Comment with 'xxx' marker")},
279 280 281
]


282
def regex_function_factory(func_name):
283
    regex = re.compile(r'\b%s\([^)]*\)' % func_name)
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
    return lambda x: regex.search(x) is not None


def regex_error_factory(description):
    return lambda: print_error(description)


std_functions = [
        ('malloc', 'Use xmalloc() in place of malloc()'),
        ('calloc', 'Use xcalloc() in place of calloc()'),
        ('realloc', 'Use xrealloc() in place of realloc()'),
        ('strdup', 'Use xstrdup() in place of strdup()'),
        ('asprintf', 'Use xasprintf() in place of asprintf()'),
        ('vasprintf', 'Use xvasprintf() in place of vasprintf()'),
        ('strcpy', 'Use ovs_strlcpy() in place of strcpy()'),
        ('strlcpy', 'Use ovs_strlcpy() in place of strlcpy()'),
        ('strncpy', 'Use ovs_strzcpy() in place of strncpy()'),
        ('strerror', 'Use ovs_strerror() in place of strerror()'),
        ('sleep', 'Use xsleep() in place of sleep()'),
        ('abort', 'Use ovs_abort() in place of abort()'),
304
        ('assert', 'Use ovs_assert() in place of assert()'),
305 306 307
        ('error', 'Use ovs_error() in place of error()'),
]
checks += [
308
    {'regex': '(\.c|\.h)(\.in)?$',
309
     'match_name': None,
310
     'prereq': lambda x: not is_comment_line(x),
311 312 313 314 315
     'check': regex_function_factory(function_name),
     'print': regex_error_factory(description)}
    for (function_name, description) in std_functions]


316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
def regex_operator_factory(operator):
    regex = re.compile(r'^[^#][^"\']*[^ "]%s[^ "\'][^"]*' % operator)
    return lambda x: regex.search(x) is not None


infix_operators = \
    [re.escape(op) for op in ['/', '%', '<<', '>>', '<=', '>=', '==', '!=',
            '^', '|', '&&', '||', '?:', '=', '+=', '-=', '*=', '/=', '%=',
            '&=', '^=', '|=', '<<=', '>>=']] \
    + ['[^<" ]<[^=" ]', '[^->" ]>[^=" ]', '[^ !()/"]\*[^/]', '[^ !&()"]&',
       '[^" +(]\+[^"+;]', '[^" -(]-[^"->;]', '[^" <>=!^|+\-*/%&]=[^"=]']
checks += [
    {'regex': '(\.c|\.h)(\.in)?$', 'match_name': None,
     'prereq': lambda x: not is_comment_line(x),
     'check': regex_operator_factory(operator),
     'print': lambda: print_warning("Line lacks whitespace around operator")}
    for operator in infix_operators]


335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
def get_file_type_checks(filename):
    """Returns the list of checks for a file based on matching the filename
       against regex."""
    global checks
    checkList = []
    for check in checks:
        if check['regex'] is None and check['match_name'] is None:
            checkList.append(check)
        if check['regex'] is not None and \
           re.compile(check['regex']).search(filename) is not None:
            checkList.append(check)
        elif check['match_name'] is not None and check['match_name'](filename):
            checkList.append(check)
    return checkList


def run_checks(current_file, line, lineno):
    """Runs the various checks for the particular line.  This will take
       filename into account."""
354
    global checking_file, total_line
355
    print_line = False
356
    for check in get_file_type_checks(current_file):
357 358
        if 'prereq' in check and not check['prereq'](line):
            continue
359
        if check['check'](line):
360
            check['print']()
361 362 363
            print_line = True

    if print_line:
364 365 366 367 368
        if checking_file:
            print("%s:%d:" % (current_file, lineno))
        else:
            print("#%d FILE: %s:%d:" % (total_line, current_file, lineno))
        print("%s\n" % line)
369 370


371 372
def ovs_checkpatch_parse(text, filename):
    global print_file_name, total_line, checking_file
373 374 375 376
    lineno = 0
    signatures = []
    co_authors = []
    parse = 0
377
    current_file = filename if checking_file else ''
378
    previous_file = ''
379 380
    scissors = re.compile(r'^[\w]*---[\w]*')
    hunks = re.compile('^(---|\+\+\+) (\S+)')
381 382
    hunk_differences = re.compile(
        r'^@@ ([0-9-+]+),([0-9-+]+) ([0-9-+]+),([0-9-+]+) @@')
383 384 385 386
    is_signature = re.compile(r'((\s*Signed-off-by: )(.*))$',
                              re.I | re.M | re.S)
    is_co_author = re.compile(r'(\s*(Co-authored-by: )(.*))$',
                              re.I | re.M | re.S)
387 388
    is_gerrit_change_id = re.compile(r'(\s*(change-id: )(.*))$',
                                     re.I | re.M | re.S)
389

390 391
    reset_counters()

392
    for line in text.split('\n'):
393 394 395
        if current_file != previous_file:
            previous_file = current_file

396
        lineno = lineno + 1
397
        total_line = total_line + 1
398 399 400
        if len(line) <= 0:
            continue

401 402 403
        if checking_file:
            parse = 2

404 405 406 407
        if parse == 1:
            match = hunks.match(line)
            if match:
                parse = parse + 1
408
                current_file = match.group(2)[2:]
409
                print_file_name = current_file
410 411 412 413 414 415 416
            continue
        elif parse == 0:
            if scissors.match(line):
                parse = parse + 1
                if not skip_signoff_check:
                    if len(signatures) == 0:
                        print_error("No signatures found.")
417
                    elif len(signatures) != 1 + len(co_authors):
418 419 420 421 422 423 424 425 426 427
                        print_error("Too many signoffs; "
                                    "are you missing Co-authored-by lines?")
                    if not set(co_authors) <= set(signatures):
                        print_error("Co-authored-by/Signed-off-by corruption")
            elif is_signature.match(line):
                m = is_signature.match(line)
                signatures.append(m.group(3))
            elif is_co_author.match(line):
                m = is_co_author.match(line)
                co_authors.append(m.group(3))
428 429 430 431
            elif is_gerrit_change_id.match(line):
                print_error(
                    "Remove Gerrit Change-Id's before submitting upstream.")
                print("%d: %s\n" % (lineno, line))
432 433 434
        elif parse == 2:
            newfile = hunks.match(line)
            if newfile:
435
                current_file = newfile.group(2)[2:]
436
                print_file_name = current_file
437
                continue
438 439 440 441 442 443 444 445
            reset_line_number = hunk_differences.match(line)
            if reset_line_number:
                lineno = int(reset_line_number.group(3))
                if lineno < 0:
                    lineno = -1 * lineno
                lineno -= 1
            if is_subtracted_line(line):
                lineno -= 1
446 447
            if not is_added_line(line):
                continue
448 449 450

            cmp_line = added_line(line)

451 452
            # Skip files which have /datapath in them, since they are
            # linux or windows coding standards
453
            if current_file.startswith('datapath'):
454
                continue
455 456
            if current_file.startswith('include/linux'):
                continue
457
            run_checks(current_file, cmp_line, lineno)
458 459 460 461 462 463
    if __errors or __warnings:
        return -1
    return 0


def usage():
464 465 466 467
    print("""\
Open vSwitch checkpatch.py
Checks a patch for trivial mistakes.
usage:
468
%s [options] [PATCH1 [PATCH2 ...] | -f SOURCE1 [SOURCE2 ...] | -1 | -2 | ...]
469 470 471 472 473 474 475 476 477 478 479 480

Input options:
-f|--check-file                Arguments are source files, not patches.
-1, -2, ...                    Check recent commits in this repo.

Check options:
-h|--help                      This help message
-b|--skip-block-whitespace     Skips the if/while/for whitespace tests
-l|--skip-leading-whitespace   Skips the leading whitespace test
-s|--skip-signoff-lines        Tolerate missing Signed-off-by line
-t|--skip-trailing-whitespace  Skips the trailing whitespace test"""
          % sys.argv[0])
481

482

483 484 485 486 487 488 489 490 491
def ovs_checkpatch_print_result(result):
    global __warnings, __errors, total_line
    if result < 0:
        print("Lines checked: %d, Warnings: %d, Errors: %d\n" %
              (total_line, __warnings, __errors))
    else:
        print("Lines checked: %d, no obvious problems found\n" % (total_line))


492 493 494 495 496 497 498 499 500 501
def ovs_checkpatch_file(filename):
    try:
        mail = email.message_from_file(open(filename, 'r'))
    except:
        print_error("Unable to parse file '%s'. Is it a patch?" % filename)
        return -1

    for part in mail.walk():
        if part.get_content_maintype() == 'multipart':
            continue
502
    result = ovs_checkpatch_parse(part.get_payload(decode=False), filename)
503
    ovs_checkpatch_print_result(result)
504
    return result
505

506

507 508 509 510 511 512 513 514 515 516 517 518 519
def partition(pred, iterable):
    """Returns [[trues], [falses]], where [trues] is the items in
    'iterable' that satisfy 'pred' and [falses] is all the rest."""
    trues = []
    falses = []
    for item in iterable:
        if pred(item):
            trues.append(item)
        else:
            falses.append(item)
    return trues, falses


520 521
if __name__ == '__main__':
    try:
522 523 524 525 526
        numeric_options, args = partition(lambda s: re.match('-[0-9]+$', s),
                                          sys.argv[1:])
        n_patches = int(numeric_options[-1][1:]) if numeric_options else 0

        optlist, args = getopt.getopt(args, 'bhlstf',
527 528
                                      ["check-file",
                                       "help",
529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548
                                       "skip-block-whitespace",
                                       "skip-leading-whitespace",
                                       "skip-signoff-lines",
                                       "skip-trailing-whitespace"])
    except:
        print("Unknown option encountered. Please rerun with -h for help.")
        sys.exit(-1)

    for o, a in optlist:
        if o in ("-h", "--help"):
            usage()
            sys.exit(0)
        elif o in ("-b", "--skip-block-whitespace"):
            skip_block_whitespace_check = True
        elif o in ("-l", "--skip-leading-whitespace"):
            skip_leading_whitespace_check = True
        elif o in ("-s", "--skip-signoff-lines"):
            skip_signoff_check = True
        elif o in ("-t", "--skip-trailing-whitespace"):
            skip_trailing_whitespace_check = True
549 550
        elif o in ("-f", "--check-file"):
            checking_file = True
551 552 553
        else:
            print("Unknown option '%s'" % o)
            sys.exit(-1)
554 555 556 557

    if sys.stdout.isatty():
        colors = True

558 559
    if n_patches:
        status = 0
560 561 562 563 564

        git_log = 'git log --no-color --no-merges --pretty=format:"%H %s" '
        with os.popen(git_log + '-%d' % n_patches, 'r') as f:
            commits = f.read().split("\n")

565
        for i in reversed(range(0, n_patches)):
566
            revision, name = commits[i].split(" ", 1)
567 568 569 570
            f = os.popen('git format-patch -1 --stdout %s' % revision, 'r')
            patch = f.read()
            f.close()

571
            print('== Checking %s ("%s") ==' % (revision[0:12], name))
572 573 574
            result = ovs_checkpatch_parse(patch, revision)
            ovs_checkpatch_print_result(result)
            if result:
575 576 577
                status = -1
        sys.exit(status)

578
    if not args:
579 580 581
        if sys.stdin.isatty():
            usage()
            sys.exit(-1)
582 583 584
        result = ovs_checkpatch_parse(sys.stdin.read(), '-')
        ovs_checkpatch_print_result(result)
        sys.exit(result)
585 586 587 588 589 590 591 592

    status = 0
    for filename in args:
        print('== Checking "%s" ==' % filename)
        result = ovs_checkpatch_file(filename)
        if result:
            status = -1
    sys.exit(status)