1# Copyright 2012 the V8 project authors. All rights reserved. 2# Redistribution and use in source and binary forms, with or without 3# modification, are permitted provided that the following conditions are 4# met: 5# 6# * Redistributions of source code must retain the above copyright 7# notice, this list of conditions and the following disclaimer. 8# * Redistributions in binary form must reproduce the above 9# copyright notice, this list of conditions and the following 10# disclaimer in the documentation and/or other materials provided 11# with the distribution. 12# * Neither the name of Google Inc. nor the names of its 13# contributors may be used to endorse or promote products derived 14# from this software without specific prior written permission. 15# 16# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 28"""Top-level presubmit script for V8. 29 30See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts 31for more details about the presubmit API built into gcl. 32""" 33 34import json 35import os 36import re 37import sys 38 39 40_EXCLUDED_PATHS = ( 41 r"^test[\\\/].*", 42 r"^testing[\\\/].*", 43 r"^third_party[\\\/].*", 44 r"^tools[\\\/].*", 45) 46 47_LICENSE_FILE = ( 48 r"LICENSE" 49) 50 51# Regular expression that matches code which should not be run through cpplint. 52_NO_LINT_PATHS = ( 53 r'src[\\\/]base[\\\/]export-template\.h', 54) 55 56 57# Regular expression that matches code only used for test binaries 58# (best effort). 59_TEST_CODE_EXCLUDED_PATHS = ( 60 r'.+-unittest\.cc', 61 # Has a method VisitForTest(). 62 r'src[\\\/]compiler[\\\/]ast-graph-builder\.cc', 63 # Test extension. 64 r'src[\\\/]extensions[\\\/]gc-extension\.cc', 65 # Runtime functions used for testing. 66 r'src[\\\/]runtime[\\\/]runtime-test\.cc', 67) 68 69 70_TEST_ONLY_WARNING = ( 71 'You might be calling functions intended only for testing from\n' 72 'production code. It is OK to ignore this warning if you know what\n' 73 'you are doing, as the heuristics used to detect the situation are\n' 74 'not perfect. The commit queue will not block on this warning.') 75 76 77def _V8PresubmitChecks(input_api, output_api): 78 """Runs the V8 presubmit checks.""" 79 import sys 80 sys.path.append(input_api.os_path.join( 81 input_api.PresubmitLocalPath(), 'tools')) 82 from v8_presubmit import CppLintProcessor 83 from v8_presubmit import JSLintProcessor 84 from v8_presubmit import TorqueLintProcessor 85 from v8_presubmit import SourceProcessor 86 from v8_presubmit import StatusFilesProcessor 87 88 def FilterFile(affected_file): 89 return input_api.FilterSourceFile( 90 affected_file, 91 files_to_check=None, 92 files_to_skip=_NO_LINT_PATHS) 93 94 def FilterTorqueFile(affected_file): 95 return input_api.FilterSourceFile( 96 affected_file, 97 files_to_check=(r'.+\.tq')) 98 99 def FilterJSFile(affected_file): 100 return input_api.FilterSourceFile( 101 affected_file, 102 files_to_check=(r'.+\.m?js')) 103 104 results = [] 105 if not CppLintProcessor().RunOnFiles( 106 input_api.AffectedFiles(file_filter=FilterFile, include_deletes=False)): 107 results.append(output_api.PresubmitError("C++ lint check failed")) 108 if not TorqueLintProcessor().RunOnFiles( 109 input_api.AffectedFiles(file_filter=FilterTorqueFile, 110 include_deletes=False)): 111 results.append(output_api.PresubmitError("Torque format check failed")) 112 if not JSLintProcessor().RunOnFiles( 113 input_api.AffectedFiles(file_filter=FilterJSFile, 114 include_deletes=False)): 115 results.append(output_api.PresubmitError("JS format check failed")) 116 if not SourceProcessor().RunOnFiles( 117 input_api.AffectedFiles(include_deletes=False)): 118 results.append(output_api.PresubmitError( 119 "Copyright header, trailing whitespaces and two empty lines " \ 120 "between declarations check failed")) 121 if not StatusFilesProcessor().RunOnFiles( 122 input_api.AffectedFiles(include_deletes=True)): 123 results.append(output_api.PresubmitError("Status file check failed")) 124 results.extend(input_api.canned_checks.CheckAuthorizedAuthor( 125 input_api, output_api, bot_allowlist=[ 126 'v8-ci-autoroll-builder@chops-service-accounts.iam.gserviceaccount.com' 127 ])) 128 return results 129 130 131def _CheckUnwantedDependencies(input_api, output_api): 132 """Runs checkdeps on #include statements added in this 133 change. Breaking - rules is an error, breaking ! rules is a 134 warning. 135 """ 136 # We need to wait until we have an input_api object and use this 137 # roundabout construct to import checkdeps because this file is 138 # eval-ed and thus doesn't have __file__. 139 original_sys_path = sys.path 140 try: 141 sys.path = sys.path + [input_api.os_path.join( 142 input_api.PresubmitLocalPath(), 'buildtools', 'checkdeps')] 143 import checkdeps 144 from cpp_checker import CppChecker 145 from rules import Rule 146 finally: 147 # Restore sys.path to what it was before. 148 sys.path = original_sys_path 149 150 def _FilesImpactedByDepsChange(files): 151 all_files = [f.AbsoluteLocalPath() for f in files] 152 deps_files = [p for p in all_files if IsDepsFile(p)] 153 impacted_files = union([_CollectImpactedFiles(path) for path in deps_files]) 154 impacted_file_objs = [ImpactedFile(path) for path in impacted_files] 155 return impacted_file_objs 156 157 def IsDepsFile(p): 158 return os.path.isfile(p) and os.path.basename(p) == 'DEPS' 159 160 def union(list_of_lists): 161 """Ensure no duplicates""" 162 return set(sum(list_of_lists, [])) 163 164 def _CollectImpactedFiles(deps_file): 165 # TODO(liviurau): Do not walk paths twice. Then we have no duplicates. 166 # Higher level DEPS changes may dominate lower level DEPS changes. 167 # TODO(liviurau): Check if DEPS changed in the right way. 168 # 'include_rules' impact c++ files but 'vars' or 'deps' do not. 169 # Maybe we just eval both old and new DEPS content and check 170 # if the list are the same. 171 result = [] 172 parent_dir = os.path.dirname(deps_file) 173 for relative_f in input_api.change.AllFiles(parent_dir): 174 abs_f = os.path.join(parent_dir, relative_f) 175 if CppChecker.IsCppFile(abs_f): 176 result.append(abs_f) 177 return result 178 179 class ImpactedFile(object): 180 """Duck type version of AffectedFile needed to check files under directories 181 where a DEPS file changed. Extend the interface along the line of 182 AffectedFile if you need it for other checks.""" 183 184 def __init__(self, path): 185 self._path = path 186 187 def LocalPath(self): 188 path = self._path.replace(os.sep, '/') 189 return os.path.normpath(path) 190 191 def ChangedContents(self): 192 with open(self._path) as f: 193 # TODO(liviurau): read only '#include' lines 194 lines = f.readlines() 195 return enumerate(lines, start=1) 196 197 def _FilterDuplicates(impacted_files, affected_files): 198 """"We include all impacted files but exclude affected files that are also 199 impacted. Files impacted by DEPS changes take precedence before files 200 affected by direct changes.""" 201 result = impacted_files[:] 202 only_paths = set([imf.LocalPath() for imf in impacted_files]) 203 for af in affected_files: 204 if not af.LocalPath() in only_paths: 205 result.append(af) 206 return result 207 208 added_includes = [] 209 affected_files = input_api.AffectedFiles() 210 impacted_by_deps = _FilesImpactedByDepsChange(affected_files) 211 for f in _FilterDuplicates(impacted_by_deps, affected_files): 212 if not CppChecker.IsCppFile(f.LocalPath()): 213 continue 214 215 changed_lines = [line for line_num, line in f.ChangedContents()] 216 added_includes.append([f.LocalPath(), changed_lines]) 217 218 deps_checker = checkdeps.DepsChecker(input_api.PresubmitLocalPath()) 219 220 error_descriptions = [] 221 warning_descriptions = [] 222 for path, rule_type, rule_description in deps_checker.CheckAddedCppIncludes( 223 added_includes): 224 description_with_path = '%s\n %s' % (path, rule_description) 225 if rule_type == Rule.DISALLOW: 226 error_descriptions.append(description_with_path) 227 else: 228 warning_descriptions.append(description_with_path) 229 230 results = [] 231 if error_descriptions: 232 results.append(output_api.PresubmitError( 233 'You added one or more #includes that violate checkdeps rules.', 234 error_descriptions)) 235 if warning_descriptions: 236 results.append(output_api.PresubmitPromptOrNotify( 237 'You added one or more #includes of files that are temporarily\n' 238 'allowed but being removed. Can you avoid introducing the\n' 239 '#include? See relevant DEPS file(s) for details and contacts.', 240 warning_descriptions)) 241 return results 242 243 244def _CheckHeadersHaveIncludeGuards(input_api, output_api): 245 """Ensures that all header files have include guards.""" 246 file_inclusion_pattern = r'src/.+\.h' 247 248 def FilterFile(affected_file): 249 files_to_skip = _EXCLUDED_PATHS + input_api.DEFAULT_FILES_TO_SKIP 250 return input_api.FilterSourceFile( 251 affected_file, 252 files_to_check=(file_inclusion_pattern, ), 253 files_to_skip=files_to_skip) 254 255 leading_src_pattern = input_api.re.compile(r'^src/') 256 dash_dot_slash_pattern = input_api.re.compile(r'[-./]') 257 def PathToGuardMacro(path): 258 """Guards should be of the form V8_PATH_TO_FILE_WITHOUT_SRC_H_.""" 259 x = input_api.re.sub(leading_src_pattern, 'v8_', path) 260 x = input_api.re.sub(dash_dot_slash_pattern, '_', x) 261 x = x.upper() + "_" 262 return x 263 264 problems = [] 265 for f in input_api.AffectedSourceFiles(FilterFile): 266 local_path = f.LocalPath() 267 guard_macro = PathToGuardMacro(local_path) 268 guard_patterns = [ 269 input_api.re.compile(r'^#ifndef ' + guard_macro + '$'), 270 input_api.re.compile(r'^#define ' + guard_macro + '$'), 271 input_api.re.compile(r'^#endif // ' + guard_macro + '$')] 272 skip_check_pattern = input_api.re.compile( 273 r'^// PRESUBMIT_INTENTIONALLY_MISSING_INCLUDE_GUARD') 274 found_patterns = [ False, False, False ] 275 file_omitted = False 276 277 for line in f.NewContents(): 278 for i in range(len(guard_patterns)): 279 if guard_patterns[i].match(line): 280 found_patterns[i] = True 281 if skip_check_pattern.match(line): 282 file_omitted = True 283 break 284 285 if not file_omitted and not all(found_patterns): 286 problems.append( 287 '%s: Missing include guard \'%s\'' % (local_path, guard_macro)) 288 289 if problems: 290 return [output_api.PresubmitError( 291 'You added one or more header files without an appropriate\n' 292 'include guard. Add the include guard {#ifndef,#define,#endif}\n' 293 'triplet or omit the check entirely through the magic comment:\n' 294 '"// PRESUBMIT_INTENTIONALLY_MISSING_INCLUDE_GUARD".', problems)] 295 else: 296 return [] 297 298 299def _CheckNoInlineHeaderIncludesInNormalHeaders(input_api, output_api): 300 """Attempts to prevent inclusion of inline headers into normal header 301 files. This tries to establish a layering where inline headers can be 302 included by other inline headers or compilation units only.""" 303 file_inclusion_pattern = r'(?!.+-inl\.h).+\.h' 304 include_directive_pattern = input_api.re.compile(r'#include ".+-inl.h"') 305 include_error = ( 306 'You are including an inline header (e.g. foo-inl.h) within a normal\n' 307 'header (e.g. bar.h) file. This violates layering of dependencies.') 308 309 def FilterFile(affected_file): 310 files_to_skip = _EXCLUDED_PATHS + input_api.DEFAULT_FILES_TO_SKIP 311 return input_api.FilterSourceFile( 312 affected_file, 313 files_to_check=(file_inclusion_pattern, ), 314 files_to_skip=files_to_skip) 315 316 problems = [] 317 for f in input_api.AffectedSourceFiles(FilterFile): 318 local_path = f.LocalPath() 319 for line_number, line in f.ChangedContents(): 320 if (include_directive_pattern.search(line)): 321 problems.append( 322 '%s:%d\n %s' % (local_path, line_number, line.strip())) 323 324 if problems: 325 return [output_api.PresubmitError(include_error, problems)] 326 else: 327 return [] 328 329 330def _CheckNoProductionCodeUsingTestOnlyFunctions(input_api, output_api): 331 """Attempts to prevent use of functions intended only for testing in 332 non-testing code. For now this is just a best-effort implementation 333 that ignores header files and may have some false positives. A 334 better implementation would probably need a proper C++ parser. 335 """ 336 # We only scan .cc files, as the declaration of for-testing functions in 337 # header files are hard to distinguish from calls to such functions without a 338 # proper C++ parser. 339 file_inclusion_pattern = r'.+\.cc' 340 341 base_function_pattern = r'[ :]test::[^\s]+|ForTest(ing)?|for_test(ing)?' 342 inclusion_pattern = input_api.re.compile(r'(%s)\s*\(' % base_function_pattern) 343 comment_pattern = input_api.re.compile(r'//.*(%s)' % base_function_pattern) 344 exclusion_pattern = input_api.re.compile( 345 r'::[A-Za-z0-9_]+(%s)|(%s)[^;]+\{' % ( 346 base_function_pattern, base_function_pattern)) 347 348 def FilterFile(affected_file): 349 files_to_skip = (_EXCLUDED_PATHS + 350 _TEST_CODE_EXCLUDED_PATHS + 351 input_api.DEFAULT_FILES_TO_SKIP) 352 return input_api.FilterSourceFile( 353 affected_file, 354 files_to_check=(file_inclusion_pattern, ), 355 files_to_skip=files_to_skip) 356 357 problems = [] 358 for f in input_api.AffectedSourceFiles(FilterFile): 359 local_path = f.LocalPath() 360 for line_number, line in f.ChangedContents(): 361 if (inclusion_pattern.search(line) and 362 not comment_pattern.search(line) and 363 not exclusion_pattern.search(line)): 364 problems.append( 365 '%s:%d\n %s' % (local_path, line_number, line.strip())) 366 367 if problems: 368 return [output_api.PresubmitPromptOrNotify(_TEST_ONLY_WARNING, problems)] 369 else: 370 return [] 371 372 373def _CheckGenderNeutralInLicenses(input_api, output_api): 374 # License files are taken as is, even if they include gendered pronouns. 375 def LicenseFilter(path): 376 input_api.FilterSourceFile(path, files_to_skip=_LICENSE_FILE) 377 378 return input_api.canned_checks.CheckGenderNeutral( 379 input_api, output_api, source_file_filter=LicenseFilter) 380 381 382def _RunTestsWithVPythonSpec(input_api, output_api): 383 return input_api.RunTests( 384 input_api.canned_checks.CheckVPythonSpec(input_api, output_api)) 385 386 387def _CommonChecks(input_api, output_api): 388 """Checks common to both upload and commit.""" 389 # TODO(machenbach): Replace some of those checks, e.g. owners and copyright, 390 # with the canned PanProjectChecks. Need to make sure that the checks all 391 # pass on all existing files. 392 checks = [ 393 input_api.canned_checks.CheckOwnersFormat, 394 input_api.canned_checks.CheckOwners, 395 _CheckCommitMessageBugEntry, 396 input_api.canned_checks.CheckPatchFormatted, 397 _CheckGenderNeutralInLicenses, 398 _V8PresubmitChecks, 399 _CheckUnwantedDependencies, 400 _CheckNoProductionCodeUsingTestOnlyFunctions, 401 _CheckHeadersHaveIncludeGuards, 402 _CheckNoInlineHeaderIncludesInNormalHeaders, 403 _CheckJSONFiles, 404 _CheckNoexceptAnnotations, 405 _RunTestsWithVPythonSpec, 406 ] 407 408 return sum([check(input_api, output_api) for check in checks], []) 409 410 411def _SkipTreeCheck(input_api, output_api): 412 """Check the env var whether we want to skip tree check. 413 Only skip if include/v8-version.h has been updated.""" 414 src_version = 'include/v8-version.h' 415 if not input_api.AffectedSourceFiles( 416 lambda file: file.LocalPath() == src_version): 417 return False 418 return input_api.environ.get('PRESUBMIT_TREE_CHECK') == 'skip' 419 420 421def _CheckCommitMessageBugEntry(input_api, output_api): 422 """Check that bug entries are well-formed in commit message.""" 423 bogus_bug_msg = ( 424 'Bogus BUG entry: %s. Please specify the issue tracker prefix and the ' 425 'issue number, separated by a colon, e.g. v8:123 or chromium:12345.') 426 results = [] 427 for bug in (input_api.change.BUG or '').split(','): 428 bug = bug.strip() 429 if 'none'.startswith(bug.lower()): 430 continue 431 if ':' not in bug: 432 try: 433 if int(bug) > 100000: 434 # Rough indicator for current chromium bugs. 435 prefix_guess = 'chromium' 436 else: 437 prefix_guess = 'v8' 438 results.append('BUG entry requires issue tracker prefix, e.g. %s:%s' % 439 (prefix_guess, bug)) 440 except ValueError: 441 results.append(bogus_bug_msg % bug) 442 elif not re.match(r'\w+:\d+', bug): 443 results.append(bogus_bug_msg % bug) 444 return [output_api.PresubmitError(r) for r in results] 445 446 447def _CheckJSONFiles(input_api, output_api): 448 def FilterFile(affected_file): 449 return input_api.FilterSourceFile( 450 affected_file, 451 files_to_check=(r'.+\.json',)) 452 453 results = [] 454 for f in input_api.AffectedFiles( 455 file_filter=FilterFile, include_deletes=False): 456 with open(f.LocalPath()) as j: 457 try: 458 json.load(j) 459 except Exception as e: 460 results.append( 461 'JSON validation failed for %s. Error:\n%s' % (f.LocalPath(), e)) 462 463 return [output_api.PresubmitError(r) for r in results] 464 465 466def _CheckNoexceptAnnotations(input_api, output_api): 467 """ 468 Checks that all user-defined constructors and assignment operators are marked 469 V8_NOEXCEPT. 470 471 This is required for standard containers to pick the right constructors. Our 472 macros (like MOVE_ONLY_WITH_DEFAULT_CONSTRUCTORS) add this automatically. 473 Omitting it at some places can result in weird compiler errors if this is 474 mixed with other classes that have the annotation. 475 476 TODO(clemensb): This check should eventually be enabled for all files via 477 tools/presubmit.py (https://crbug.com/v8/8616). 478 """ 479 480 def FilterFile(affected_file): 481 return input_api.FilterSourceFile( 482 affected_file, 483 files_to_check=(r'src/.*', r'test/.*')) 484 485 486 # matches any class name. 487 class_name = r'\b([A-Z][A-Za-z0-9_:]*)(?:::\1)?' 488 # initial class name is potentially followed by this to declare an assignment 489 # operator. 490 potential_assignment = r'(?:&\s+(?:\1::)?operator=)?\s*' 491 # matches an argument list that contains only a reference to a class named 492 # like the first capture group, potentially const. 493 single_class_ref_arg = r'\(\s*(?:const\s+)?\1(?:::\1)?&&?[^,;)]*\)' 494 # matches anything but a sequence of whitespaces followed by either 495 # V8_NOEXCEPT or "= delete". 496 not_followed_by_noexcept = r'(?!\s+(?:V8_NOEXCEPT|=\s+delete)\b)' 497 full_pattern = r'^.*?' + class_name + potential_assignment + \ 498 single_class_ref_arg + not_followed_by_noexcept + '.*?$' 499 regexp = input_api.re.compile(full_pattern, re.MULTILINE) 500 501 errors = [] 502 for f in input_api.AffectedFiles(file_filter=FilterFile, 503 include_deletes=False): 504 with open(f.LocalPath()) as fh: 505 for match in re.finditer(regexp, fh.read()): 506 errors.append('in {}: {}'.format(f.LocalPath(), 507 match.group().strip())) 508 509 if errors: 510 return [output_api.PresubmitPromptOrNotify( 511 'Copy constructors, move constructors, copy assignment operators and ' 512 'move assignment operators should be marked V8_NOEXCEPT.\n' 513 'Please report false positives on https://crbug.com/v8/8616.', 514 errors)] 515 return [] 516 517 518def CheckChangeOnUpload(input_api, output_api): 519 results = [] 520 results.extend(_CommonChecks(input_api, output_api)) 521 return results 522 523 524def CheckChangeOnCommit(input_api, output_api): 525 results = [] 526 results.extend(_CommonChecks(input_api, output_api)) 527 results.extend(input_api.canned_checks.CheckChangeHasDescription( 528 input_api, output_api)) 529 if not _SkipTreeCheck(input_api, output_api): 530 results.extend(input_api.canned_checks.CheckTreeIsOpen( 531 input_api, output_api, 532 json_url='http://v8-status.appspot.com/current?format=json')) 533 return results 534