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