1# -*- coding:utf-8 -*- 2# Copyright 2016 The Android Open Source Project 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 16"""Functions that implement the actual checks.""" 17 18from __future__ import print_function 19 20import json 21import os 22import platform 23import re 24import sys 25 26_path = os.path.realpath(__file__ + '/../..') 27if sys.path[0] != _path: 28 sys.path.insert(0, _path) 29del _path 30 31# pylint: disable=wrong-import-position 32import rh.git 33import rh.results 34from rh.sixish import string_types 35import rh.utils 36 37 38class Placeholders(object): 39 """Holder class for replacing ${vars} in arg lists. 40 41 To add a new variable to replace in config files, just add it as a @property 42 to this class using the form. So to add support for BIRD: 43 @property 44 def var_BIRD(self): 45 return <whatever this is> 46 47 You can return either a string or an iterable (e.g. a list or tuple). 48 """ 49 50 def __init__(self, diff=()): 51 """Initialize. 52 53 Args: 54 diff: The list of files that changed. 55 """ 56 self.diff = diff 57 58 def expand_vars(self, args): 59 """Perform place holder expansion on all of |args|. 60 61 Args: 62 args: The args to perform expansion on. 63 64 Returns: 65 The updated |args| list. 66 """ 67 all_vars = set(self.vars()) 68 replacements = dict((var, self.get(var)) for var in all_vars) 69 70 ret = [] 71 for arg in args: 72 if arg.endswith('${PREUPLOAD_FILES_PREFIXED}'): 73 if arg == '${PREUPLOAD_FILES_PREFIXED}': 74 assert len(ret) > 1, ('PREUPLOAD_FILES_PREFIXED cannot be ' 75 'the 1st or 2nd argument') 76 prev_arg = ret[-1] 77 ret = ret[0:-1] 78 for file in self.get('PREUPLOAD_FILES'): 79 ret.append(prev_arg) 80 ret.append(file) 81 else: 82 prefix = arg[0:-len('${PREUPLOAD_FILES_PREFIXED}')] 83 ret.extend( 84 prefix + file for file in self.get('PREUPLOAD_FILES')) 85 else: 86 # First scan for exact matches 87 for key, val in replacements.items(): 88 var = '${%s}' % (key,) 89 if arg == var: 90 if isinstance(val, string_types): 91 ret.append(val) 92 else: 93 ret.extend(val) 94 # We break on first hit to avoid double expansion. 95 break 96 else: 97 # If no exact matches, do an inline replacement. 98 def replace(m): 99 val = self.get(m.group(1)) 100 if isinstance(val, string_types): 101 return val 102 return ' '.join(val) 103 ret.append(re.sub(r'\$\{(%s)\}' % ('|'.join(all_vars),), 104 replace, arg)) 105 return ret 106 107 @classmethod 108 def vars(cls): 109 """Yield all replacement variable names.""" 110 for key in dir(cls): 111 if key.startswith('var_'): 112 yield key[4:] 113 114 def get(self, var): 115 """Helper function to get the replacement |var| value.""" 116 return getattr(self, 'var_%s' % (var,)) 117 118 @property 119 def var_PREUPLOAD_COMMIT_MESSAGE(self): 120 """The git commit message.""" 121 return os.environ.get('PREUPLOAD_COMMIT_MESSAGE', '') 122 123 @property 124 def var_PREUPLOAD_COMMIT(self): 125 """The git commit sha1.""" 126 return os.environ.get('PREUPLOAD_COMMIT', '') 127 128 @property 129 def var_PREUPLOAD_FILES(self): 130 """List of files modified in this git commit.""" 131 return [x.file for x in self.diff if x.status != 'D'] 132 133 @property 134 def var_REPO_ROOT(self): 135 """The root of the repo checkout.""" 136 return rh.git.find_repo_root() 137 138 @property 139 def var_BUILD_OS(self): 140 """The build OS (see _get_build_os_name for details).""" 141 return _get_build_os_name() 142 143 144class HookOptions(object): 145 """Holder class for hook options.""" 146 147 def __init__(self, name, args, tool_paths): 148 """Initialize. 149 150 Args: 151 name: The name of the hook. 152 args: The override commandline arguments for the hook. 153 tool_paths: A dictionary with tool names to paths. 154 """ 155 self.name = name 156 self._args = args 157 self._tool_paths = tool_paths 158 159 @staticmethod 160 def expand_vars(args, diff=()): 161 """Perform place holder expansion on all of |args|.""" 162 replacer = Placeholders(diff=diff) 163 return replacer.expand_vars(args) 164 165 def args(self, default_args=(), diff=()): 166 """Gets the hook arguments, after performing place holder expansion. 167 168 Args: 169 default_args: The list to return if |self._args| is empty. 170 diff: The list of files that changed in the current commit. 171 172 Returns: 173 A list with arguments. 174 """ 175 args = self._args 176 if not args: 177 args = default_args 178 179 return self.expand_vars(args, diff=diff) 180 181 def tool_path(self, tool_name): 182 """Gets the path in which the |tool_name| executable can be found. 183 184 This function performs expansion for some place holders. If the tool 185 does not exist in the overridden |self._tool_paths| dictionary, the tool 186 name will be returned and will be run from the user's $PATH. 187 188 Args: 189 tool_name: The name of the executable. 190 191 Returns: 192 The path of the tool with all optional place holders expanded. 193 """ 194 assert tool_name in TOOL_PATHS 195 if tool_name not in self._tool_paths: 196 return TOOL_PATHS[tool_name] 197 198 tool_path = os.path.normpath(self._tool_paths[tool_name]) 199 return self.expand_vars([tool_path])[0] 200 201 202def _run(cmd, **kwargs): 203 """Helper command for checks that tend to gather output.""" 204 kwargs.setdefault('combine_stdout_stderr', True) 205 kwargs.setdefault('capture_output', True) 206 kwargs.setdefault('check', False) 207 # Make sure hooks run with stdin disconnected to avoid accidentally 208 # interactive tools causing pauses. 209 kwargs.setdefault('input', '') 210 return rh.utils.run(cmd, **kwargs) 211 212 213def _match_regex_list(subject, expressions): 214 """Try to match a list of regular expressions to a string. 215 216 Args: 217 subject: The string to match regexes on. 218 expressions: An iterable of regular expressions to check for matches with. 219 220 Returns: 221 Whether the passed in subject matches any of the passed in regexes. 222 """ 223 for expr in expressions: 224 if re.search(expr, subject): 225 return True 226 return False 227 228 229def _filter_diff(diff, include_list, exclude_list=()): 230 """Filter out files based on the conditions passed in. 231 232 Args: 233 diff: list of diff objects to filter. 234 include_list: list of regex that when matched with a file path will cause 235 it to be added to the output list unless the file is also matched with 236 a regex in the exclude_list. 237 exclude_list: list of regex that when matched with a file will prevent it 238 from being added to the output list, even if it is also matched with a 239 regex in the include_list. 240 241 Returns: 242 A list of filepaths that contain files matched in the include_list and not 243 in the exclude_list. 244 """ 245 filtered = [] 246 for d in diff: 247 if (d.status != 'D' and 248 _match_regex_list(d.file, include_list) and 249 not _match_regex_list(d.file, exclude_list)): 250 # We've got a match! 251 filtered.append(d) 252 return filtered 253 254 255def _get_build_os_name(): 256 """Gets the build OS name. 257 258 Returns: 259 A string in a format usable to get prebuilt tool paths. 260 """ 261 system = platform.system() 262 if 'Darwin' in system or 'Macintosh' in system: 263 return 'darwin-x86' 264 265 # TODO: Add more values if needed. 266 return 'linux-x86' 267 268 269def _fixup_func_caller(cmd, **kwargs): 270 """Wraps |cmd| around a callable automated fixup. 271 272 For hooks that support automatically fixing errors after running (e.g. code 273 formatters), this function provides a way to run |cmd| as the |fixup_func| 274 parameter in HookCommandResult. 275 """ 276 def wrapper(): 277 result = _run(cmd, **kwargs) 278 if result.returncode not in (None, 0): 279 return result.stdout 280 return None 281 return wrapper 282 283 284def _check_cmd(hook_name, project, commit, cmd, fixup_func=None, **kwargs): 285 """Runs |cmd| and returns its result as a HookCommandResult.""" 286 return [rh.results.HookCommandResult(hook_name, project, commit, 287 _run(cmd, **kwargs), 288 fixup_func=fixup_func)] 289 290 291# Where helper programs exist. 292TOOLS_DIR = os.path.realpath(__file__ + '/../../tools') 293 294def get_helper_path(tool): 295 """Return the full path to the helper |tool|.""" 296 return os.path.join(TOOLS_DIR, tool) 297 298 299def check_custom(project, commit, _desc, diff, options=None, **kwargs): 300 """Run a custom hook.""" 301 return _check_cmd(options.name, project, commit, options.args((), diff), 302 **kwargs) 303 304 305def check_bpfmt(project, commit, _desc, diff, options=None): 306 """Checks that Blueprint files are formatted with bpfmt.""" 307 filtered = _filter_diff(diff, [r'\.bp$']) 308 if not filtered: 309 return None 310 311 bpfmt = options.tool_path('bpfmt') 312 cmd = [bpfmt, '-l'] + options.args((), filtered) 313 ret = [] 314 for d in filtered: 315 data = rh.git.get_file_content(commit, d.file) 316 result = _run(cmd, input=data) 317 if result.stdout: 318 ret.append(rh.results.HookResult( 319 'bpfmt', project, commit, error=result.stdout, 320 files=(d.file,))) 321 return ret 322 323 324def check_checkpatch(project, commit, _desc, diff, options=None): 325 """Run |diff| through the kernel's checkpatch.pl tool.""" 326 tool = get_helper_path('checkpatch.pl') 327 cmd = ([tool, '-', '--root', project.dir] + 328 options.args(('--ignore=GERRIT_CHANGE_ID',), diff)) 329 return _check_cmd('checkpatch.pl', project, commit, cmd, 330 input=rh.git.get_patch(commit)) 331 332 333def check_clang_format(project, commit, _desc, diff, options=None): 334 """Run git clang-format on the commit.""" 335 tool = get_helper_path('clang-format.py') 336 clang_format = options.tool_path('clang-format') 337 git_clang_format = options.tool_path('git-clang-format') 338 tool_args = (['--clang-format', clang_format, '--git-clang-format', 339 git_clang_format] + 340 options.args(('--style', 'file', '--commit', commit), diff)) 341 cmd = [tool] + tool_args 342 fixup_func = _fixup_func_caller([tool, '--fix'] + tool_args) 343 return _check_cmd('clang-format', project, commit, cmd, 344 fixup_func=fixup_func) 345 346 347def check_google_java_format(project, commit, _desc, _diff, options=None): 348 """Run google-java-format on the commit.""" 349 350 tool = get_helper_path('google-java-format.py') 351 google_java_format = options.tool_path('google-java-format') 352 google_java_format_diff = options.tool_path('google-java-format-diff') 353 tool_args = ['--google-java-format', google_java_format, 354 '--google-java-format-diff', google_java_format_diff, 355 '--commit', commit] + options.args() 356 cmd = [tool] + tool_args 357 fixup_func = _fixup_func_caller([tool, '--fix'] + tool_args) 358 return _check_cmd('google-java-format', project, commit, cmd, 359 fixup_func=fixup_func) 360 361 362def check_commit_msg_bug_field(project, commit, desc, _diff, options=None): 363 """Check the commit message for a 'Bug:' line.""" 364 field = 'Bug' 365 regex = r'^%s: (None|[0-9]+(, [0-9]+)*)$' % (field,) 366 check_re = re.compile(regex) 367 368 if options.args(): 369 raise ValueError('commit msg %s check takes no options' % (field,)) 370 371 found = [] 372 for line in desc.splitlines(): 373 if check_re.match(line): 374 found.append(line) 375 376 if not found: 377 error = ('Commit message is missing a "%s:" line. It must match the\n' 378 'following case-sensitive regex:\n\n %s') % (field, regex) 379 else: 380 return None 381 382 return [rh.results.HookResult('commit msg: "%s:" check' % (field,), 383 project, commit, error=error)] 384 385 386def check_commit_msg_changeid_field(project, commit, desc, _diff, options=None): 387 """Check the commit message for a 'Change-Id:' line.""" 388 field = 'Change-Id' 389 regex = r'^%s: I[a-f0-9]+$' % (field,) 390 check_re = re.compile(regex) 391 392 if options.args(): 393 raise ValueError('commit msg %s check takes no options' % (field,)) 394 395 found = [] 396 for line in desc.splitlines(): 397 if check_re.match(line): 398 found.append(line) 399 400 if not found: 401 error = ('Commit message is missing a "%s:" line. It must match the\n' 402 'following case-sensitive regex:\n\n %s') % (field, regex) 403 elif len(found) > 1: 404 error = ('Commit message has too many "%s:" lines. There can be only ' 405 'one.') % (field,) 406 else: 407 return None 408 409 return [rh.results.HookResult('commit msg: "%s:" check' % (field,), 410 project, commit, error=error)] 411 412 413PREBUILT_APK_MSG = """Commit message is missing required prebuilt APK 414information. To generate the information, use the aapt tool to dump badging 415information of the APKs being uploaded, specify where the APK was built, and 416specify whether the APKs are suitable for release: 417 418 for apk in $(find . -name '*.apk' | sort); do 419 echo "${apk}" 420 ${AAPT} dump badging "${apk}" | 421 grep -iE "(package: |sdkVersion:|targetSdkVersion:)" | 422 sed -e "s/' /'\\n/g" 423 echo 424 done 425 426It must match the following case-sensitive multiline regex searches: 427 428 %s 429 430For more information, see go/platform-prebuilt and go/android-prebuilt. 431 432""" 433 434 435def check_commit_msg_prebuilt_apk_fields(project, commit, desc, diff, 436 options=None): 437 """Check that prebuilt APK commits contain the required lines.""" 438 439 if options.args(): 440 raise ValueError('prebuilt apk check takes no options') 441 442 filtered = _filter_diff(diff, [r'\.apk$']) 443 if not filtered: 444 return None 445 446 regexes = [ 447 r'^package: .*$', 448 r'^sdkVersion:.*$', 449 r'^targetSdkVersion:.*$', 450 r'^Built here:.*$', 451 (r'^This build IS( NOT)? suitable for' 452 r'( preview|( preview or)? public) release' 453 r'( but IS NOT suitable for public release)?\.$') 454 ] 455 456 missing = [] 457 for regex in regexes: 458 if not re.search(regex, desc, re.MULTILINE): 459 missing.append(regex) 460 461 if missing: 462 error = PREBUILT_APK_MSG % '\n '.join(missing) 463 else: 464 return None 465 466 return [rh.results.HookResult('commit msg: "prebuilt apk:" check', 467 project, commit, error=error)] 468 469 470TEST_MSG = """Commit message is missing a "Test:" line. It must match the 471following case-sensitive regex: 472 473 %s 474 475The Test: stanza is free-form and should describe how you tested your change. 476As a CL author, you'll have a consistent place to describe the testing strategy 477you use for your work. As a CL reviewer, you'll be reminded to discuss testing 478as part of your code review, and you'll more easily replicate testing when you 479patch in CLs locally. 480 481Some examples below: 482 483Test: make WITH_TIDY=1 mmma art 484Test: make test-art 485Test: manual - took a photo 486Test: refactoring CL. Existing unit tests still pass. 487 488Check the git history for more examples. It's a free-form field, so we urge 489you to develop conventions that make sense for your project. Note that many 490projects use exact test commands, which are perfectly fine. 491 492Adding good automated tests with new code is critical to our goals of keeping 493the system stable and constantly improving quality. Please use Test: to 494highlight this area of your development. And reviewers, please insist on 495high-quality Test: descriptions. 496""" 497 498 499def check_commit_msg_test_field(project, commit, desc, _diff, options=None): 500 """Check the commit message for a 'Test:' line.""" 501 field = 'Test' 502 regex = r'^%s: .*$' % (field,) 503 check_re = re.compile(regex) 504 505 if options.args(): 506 raise ValueError('commit msg %s check takes no options' % (field,)) 507 508 found = [] 509 for line in desc.splitlines(): 510 if check_re.match(line): 511 found.append(line) 512 513 if not found: 514 error = TEST_MSG % (regex) 515 else: 516 return None 517 518 return [rh.results.HookResult('commit msg: "%s:" check' % (field,), 519 project, commit, error=error)] 520 521 522RELNOTE_MISSPELL_MSG = """Commit message contains something that looks 523similar to the "Relnote:" tag. It must match the regex: 524 525 %s 526 527The Relnote: stanza is free-form and should describe what developers need to 528know about your change. 529 530Some examples below: 531 532Relnote: "Added a new API `Class#isBetter` to determine whether or not the 533class is better" 534Relnote: Fixed an issue where the UI would hang on a double tap. 535 536Check the git history for more examples. It's a free-form field, so we urge 537you to develop conventions that make sense for your project. 538""" 539 540RELNOTE_MISSING_QUOTES_MSG = """Commit message contains something that looks 541similar to the "Relnote:" tag but might be malformatted. For multiline 542release notes, you need to include a starting and closing quote. 543 544Multi-line Relnote example: 545 546Relnote: "Added a new API `Class#getSize` to get the size of the class. 547This is useful if you need to know the size of the class." 548 549Single-line Relnote example: 550 551Relnote: Added a new API `Class#containsData` 552""" 553 554def check_commit_msg_relnote_field_format(project, commit, desc, _diff, 555 options=None): 556 """Check the commit for one correctly formatted 'Relnote:' line. 557 558 Checks the commit message for two things: 559 (1) Checks for possible misspellings of the 'Relnote:' tag. 560 (2) Ensures that multiline release notes are properly formatted with a 561 starting quote and an endling quote. 562 """ 563 field = 'Relnote' 564 regex_relnote = r'^%s:.*$' % (field,) 565 check_re_relnote = re.compile(regex_relnote, re.IGNORECASE) 566 567 if options.args(): 568 raise ValueError('commit msg %s check takes no options' % (field,)) 569 570 # Check 1: Check for possible misspellings of the `Relnote:` field. 571 572 # Regex for misspelled fields. 573 possible_field_misspells = {'Relnotes', 'ReleaseNote', 574 'Rel-note', 'Rel note', 575 'rel-notes', 'releasenotes', 576 'release-note', 'release-notes'} 577 regex_field_misspells = r'^(%s): .*$' % ( 578 '|'.join(possible_field_misspells), 579 ) 580 check_re_field_misspells = re.compile(regex_field_misspells, re.IGNORECASE) 581 582 ret = [] 583 for line in desc.splitlines(): 584 if check_re_field_misspells.match(line): 585 error = RELNOTE_MISSPELL_MSG % (regex_relnote, ) 586 ret.append( 587 rh.results.HookResult(('commit msg: "%s:" ' 588 'tag spelling error') % (field,), 589 project, commit, error=error)) 590 591 # Check 2: Check that multiline Relnotes are quoted. 592 593 check_re_empty_string = re.compile(r'^$') 594 595 # Regex to find other fields that could be used. 596 regex_other_fields = r'^[a-zA-Z0-9-]+:' 597 check_re_other_fields = re.compile(regex_other_fields) 598 599 desc_lines = desc.splitlines() 600 for i, cur_line in enumerate(desc_lines): 601 # Look for a Relnote tag that is before the last line and 602 # lacking any quotes. 603 if (check_re_relnote.match(cur_line) and 604 i < len(desc_lines) - 1 and 605 '"' not in cur_line): 606 next_line = desc_lines[i + 1] 607 # Check that the next line does not contain any other field 608 # and it's not an empty string. 609 if (not check_re_other_fields.findall(next_line) and 610 not check_re_empty_string.match(next_line)): 611 ret.append( 612 rh.results.HookResult(('commit msg: "%s:" ' 613 'tag missing quotes') % (field,), 614 project, commit, 615 error=RELNOTE_MISSING_QUOTES_MSG)) 616 break 617 618 # Check 3: Check that multiline Relnotes contain matching quotes. 619 620 first_quote_found = False 621 second_quote_found = False 622 for cur_line in desc_lines: 623 contains_quote = '"' in cur_line 624 contains_field = check_re_other_fields.findall(cur_line) 625 # If we have found the first quote and another field, break and fail. 626 if first_quote_found and contains_field: 627 break 628 # If we have found the first quote, this line contains a quote, 629 # and this line is not another field, break and succeed. 630 if first_quote_found and contains_quote: 631 second_quote_found = True 632 break 633 # Check that the `Relnote:` tag exists and it contains a starting quote. 634 if check_re_relnote.match(cur_line) and contains_quote: 635 first_quote_found = True 636 # A single-line Relnote containing a start and ending quote 637 # is valid as well. 638 if cur_line.count('"') == 2: 639 second_quote_found = True 640 break 641 642 if first_quote_found != second_quote_found: 643 ret.append( 644 rh.results.HookResult(('commit msg: "%s:" ' 645 'tag missing closing quote') % (field,), 646 project, commit, 647 error=RELNOTE_MISSING_QUOTES_MSG)) 648 return ret 649 650 651RELNOTE_REQUIRED_CURRENT_TXT_MSG = """\ 652Commit contains a change to current.txt or public_plus_experimental_current.txt, 653but the commit message does not contain the required `Relnote:` tag. It must 654match the regex: 655 656 %s 657 658The Relnote: stanza is free-form and should describe what developers need to 659know about your change. If you are making infrastructure changes, you 660can set the Relnote: stanza to be "N/A" for the commit to not be included 661in release notes. 662 663Some examples: 664 665Relnote: "Added a new API `Class#isBetter` to determine whether or not the 666class is better" 667Relnote: Fixed an issue where the UI would hang on a double tap. 668Relnote: N/A 669 670Check the git history for more examples. 671""" 672 673def check_commit_msg_relnote_for_current_txt(project, commit, desc, diff, 674 options=None): 675 """Check changes to current.txt contain the 'Relnote:' stanza.""" 676 field = 'Relnote' 677 regex = r'^%s: .+$' % (field,) 678 check_re = re.compile(regex, re.IGNORECASE) 679 680 if options.args(): 681 raise ValueError('commit msg %s check takes no options' % (field,)) 682 683 filtered = _filter_diff( 684 diff, 685 [r'(^|/)(public_plus_experimental_current|current)\.txt$'] 686 ) 687 # If the commit does not contain a change to *current.txt, then this repo 688 # hook check no longer applies. 689 if not filtered: 690 return None 691 692 found = [] 693 for line in desc.splitlines(): 694 if check_re.match(line): 695 found.append(line) 696 697 if not found: 698 error = RELNOTE_REQUIRED_CURRENT_TXT_MSG % (regex) 699 else: 700 return None 701 702 return [rh.results.HookResult('commit msg: "%s:" check' % (field,), 703 project, commit, error=error)] 704 705 706def check_cpplint(project, commit, _desc, diff, options=None): 707 """Run cpplint.""" 708 # This list matches what cpplint expects. We could run on more (like .cxx), 709 # but cpplint would just ignore them. 710 filtered = _filter_diff(diff, [r'\.(cc|h|cpp|cu|cuh)$']) 711 if not filtered: 712 return None 713 714 cpplint = options.tool_path('cpplint') 715 cmd = [cpplint] + options.args(('${PREUPLOAD_FILES}',), filtered) 716 return _check_cmd('cpplint', project, commit, cmd) 717 718 719def check_gofmt(project, commit, _desc, diff, options=None): 720 """Checks that Go files are formatted with gofmt.""" 721 filtered = _filter_diff(diff, [r'\.go$']) 722 if not filtered: 723 return None 724 725 gofmt = options.tool_path('gofmt') 726 cmd = [gofmt, '-l'] + options.args((), filtered) 727 ret = [] 728 for d in filtered: 729 data = rh.git.get_file_content(commit, d.file) 730 result = _run(cmd, input=data) 731 if result.stdout: 732 ret.append(rh.results.HookResult( 733 'gofmt', project, commit, error=result.stdout, 734 files=(d.file,))) 735 return ret 736 737 738def check_json(project, commit, _desc, diff, options=None): 739 """Verify json files are valid.""" 740 if options.args(): 741 raise ValueError('json check takes no options') 742 743 filtered = _filter_diff(diff, [r'\.json$']) 744 if not filtered: 745 return None 746 747 ret = [] 748 for d in filtered: 749 data = rh.git.get_file_content(commit, d.file) 750 try: 751 json.loads(data) 752 except ValueError as e: 753 ret.append(rh.results.HookResult( 754 'json', project, commit, error=str(e), 755 files=(d.file,))) 756 return ret 757 758 759def _check_pylint(project, commit, _desc, diff, extra_args=None, options=None): 760 """Run pylint.""" 761 filtered = _filter_diff(diff, [r'\.py$']) 762 if not filtered: 763 return None 764 765 if extra_args is None: 766 extra_args = [] 767 768 pylint = options.tool_path('pylint') 769 cmd = [ 770 get_helper_path('pylint.py'), 771 '--executable-path', pylint, 772 ] + extra_args + options.args(('${PREUPLOAD_FILES}',), filtered) 773 return _check_cmd('pylint', project, commit, cmd) 774 775 776def check_pylint2(project, commit, desc, diff, options=None): 777 """Run pylint through Python 2.""" 778 return _check_pylint(project, commit, desc, diff, options=options) 779 780 781def check_pylint3(project, commit, desc, diff, options=None): 782 """Run pylint through Python 3.""" 783 return _check_pylint(project, commit, desc, diff, 784 extra_args=['--py3'], 785 options=options) 786 787 788def check_rustfmt(project, commit, _desc, diff, options=None): 789 """Run "rustfmt --check" on diffed rust files""" 790 filtered = _filter_diff(diff, [r'\.rs$']) 791 if not filtered: 792 return None 793 794 rustfmt = options.tool_path('rustfmt') 795 cmd = [rustfmt] + options.args(('--check', '${PREUPLOAD_FILES}',), filtered) 796 return _check_cmd('rustfmt', project, commit, cmd) 797 798 799def check_xmllint(project, commit, _desc, diff, options=None): 800 """Run xmllint.""" 801 # XXX: Should we drop most of these and probe for <?xml> tags? 802 extensions = frozenset(( 803 'dbus-xml', # Generated DBUS interface. 804 'dia', # File format for Dia. 805 'dtd', # Document Type Definition. 806 'fml', # Fuzzy markup language. 807 'form', # Forms created by IntelliJ GUI Designer. 808 'fxml', # JavaFX user interfaces. 809 'glade', # Glade user interface design. 810 'grd', # GRIT translation files. 811 'iml', # Android build modules? 812 'kml', # Keyhole Markup Language. 813 'mxml', # Macromedia user interface markup language. 814 'nib', # OS X Cocoa Interface Builder. 815 'plist', # Property list (for OS X). 816 'pom', # Project Object Model (for Apache Maven). 817 'rng', # RELAX NG schemas. 818 'sgml', # Standard Generalized Markup Language. 819 'svg', # Scalable Vector Graphics. 820 'uml', # Unified Modeling Language. 821 'vcproj', # Microsoft Visual Studio project. 822 'vcxproj', # Microsoft Visual Studio project. 823 'wxs', # WiX Transform File. 824 'xhtml', # XML HTML. 825 'xib', # OS X Cocoa Interface Builder. 826 'xlb', # Android locale bundle. 827 'xml', # Extensible Markup Language. 828 'xsd', # XML Schema Definition. 829 'xsl', # Extensible Stylesheet Language. 830 )) 831 832 filtered = _filter_diff(diff, [r'\.(%s)$' % '|'.join(extensions)]) 833 if not filtered: 834 return None 835 836 # TODO: Figure out how to integrate schema validation. 837 # XXX: Should we use python's XML libs instead? 838 cmd = ['xmllint'] + options.args(('${PREUPLOAD_FILES}',), filtered) 839 840 return _check_cmd('xmllint', project, commit, cmd) 841 842 843def check_android_test_mapping(project, commit, _desc, diff, options=None): 844 """Verify Android TEST_MAPPING files are valid.""" 845 if options.args(): 846 raise ValueError('Android TEST_MAPPING check takes no options') 847 filtered = _filter_diff(diff, [r'(^|.*/)TEST_MAPPING$']) 848 if not filtered: 849 return None 850 851 testmapping_format = options.tool_path('android-test-mapping-format') 852 testmapping_args = ['--commit', commit] 853 cmd = [testmapping_format] + options.args( 854 (project.dir, '${PREUPLOAD_FILES}'), filtered) + testmapping_args 855 return _check_cmd('android-test-mapping-format', project, commit, cmd) 856 857 858# Hooks that projects can opt into. 859# Note: Make sure to keep the top level README.md up to date when adding more! 860BUILTIN_HOOKS = { 861 'android_test_mapping_format': check_android_test_mapping, 862 'bpfmt': check_bpfmt, 863 'checkpatch': check_checkpatch, 864 'clang_format': check_clang_format, 865 'commit_msg_bug_field': check_commit_msg_bug_field, 866 'commit_msg_changeid_field': check_commit_msg_changeid_field, 867 'commit_msg_prebuilt_apk_fields': check_commit_msg_prebuilt_apk_fields, 868 'commit_msg_test_field': check_commit_msg_test_field, 869 'commit_msg_relnote_field_format': check_commit_msg_relnote_field_format, 870 'commit_msg_relnote_for_current_txt': 871 check_commit_msg_relnote_for_current_txt, 872 'cpplint': check_cpplint, 873 'gofmt': check_gofmt, 874 'google_java_format': check_google_java_format, 875 'jsonlint': check_json, 876 'pylint': check_pylint2, 877 'pylint2': check_pylint2, 878 'pylint3': check_pylint3, 879 'rustfmt': check_rustfmt, 880 'xmllint': check_xmllint, 881} 882 883# Additional tools that the hooks can call with their default values. 884# Note: Make sure to keep the top level README.md up to date when adding more! 885TOOL_PATHS = { 886 'android-test-mapping-format': 887 os.path.join(TOOLS_DIR, 'android_test_mapping_format.py'), 888 'bpfmt': 'bpfmt', 889 'clang-format': 'clang-format', 890 'cpplint': os.path.join(TOOLS_DIR, 'cpplint.py'), 891 'git-clang-format': 'git-clang-format', 892 'gofmt': 'gofmt', 893 'google-java-format': 'google-java-format', 894 'google-java-format-diff': 'google-java-format-diff.py', 895 'pylint': 'pylint', 896 'rustfmt': 'rustfmt', 897} 898