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
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.
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.
28 """Top-level presubmit script for V8.
30 See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
31 for more details about the presubmit API built into gcl.
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.
46 r"^third_party[\\\/].*",
54 # Regular expression that matches code which should not be run through cpplint.
56 r'src[\\\/]base[\\\/]export-template\.h',
60 # Regular expression that matches code only used for test binaries
62 _TEST_CODE_EXCLUDED_PATHS = (
64 # Has a method VisitForTest().
65 r'src[\\\/]compiler[\\\/]ast-graph-builder\.cc',
67 r'src[\\\/]extensions[\\\/]gc-extension\.cc',
68 # Runtime functions used for testing.
69 r'src[\\\/]runtime[\\\/]runtime-test\.cc',
71 r'src[\\\/]heap[\\\/]cppgc[\\\/]testing\.cc',
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.')
82 def _V8PresubmitChecks(input_api, output_api):
83 """Runs the V8 presubmit checks."""
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 GCMoleProcessor
89 from v8_presubmit import JSLintProcessor
90 from v8_presubmit import TorqueLintProcessor
91 from v8_presubmit import SourceProcessor
92 from v8_presubmit import StatusFilesProcessor
94 def FilterFile(affected_file):
95 return input_api.FilterSourceFile(
98 files_to_skip=_NO_LINT_PATHS)
100 def FilterTorqueFile(affected_file):
101 return input_api.FilterSourceFile(
103 files_to_check=(r'.+\.tq'))
105 def FilterJSFile(affected_file):
106 return input_api.FilterSourceFile(
108 files_to_check=(r'.+\.m?js'))
111 if not CppLintProcessor().RunOnFiles(
112 input_api.AffectedFiles(file_filter=FilterFile, include_deletes=False)):
113 results.append(output_api.PresubmitError("C++ lint check failed"))
114 if not TorqueLintProcessor().RunOnFiles(
115 input_api.AffectedFiles(file_filter=FilterTorqueFile,
116 include_deletes=False)):
117 results.append(output_api.PresubmitError("Torque format check failed"))
118 if not JSLintProcessor().RunOnFiles(
119 input_api.AffectedFiles(file_filter=FilterJSFile,
120 include_deletes=False)):
121 results.append(output_api.PresubmitError("JS format check failed"))
122 if not SourceProcessor().RunOnFiles(
123 input_api.AffectedFiles(include_deletes=False)):
124 results.append(output_api.PresubmitError(
125 "Copyright header, trailing whitespaces and two empty lines " \
126 "between declarations check failed"))
127 if not StatusFilesProcessor().RunOnFiles(
128 input_api.AffectedFiles(include_deletes=True)):
129 results.append(output_api.PresubmitError("Status file check failed"))
130 if not GCMoleProcessor().RunOnFiles(
131 input_api.AffectedFiles(include_deletes=False)):
132 results.append(output_api.PresubmitError("GCMole pattern check failed"))
133 results.extend(input_api.canned_checks.CheckAuthorizedAuthor(
134 input_api, output_api, bot_allowlist=[
135 'v8-ci-autoroll-builder@chops-service-accounts.iam.gserviceaccount.com'
140 def _CheckUnwantedDependencies(input_api, output_api):
141 """Runs checkdeps on #include statements added in this
142 change. Breaking - rules is an error, breaking ! rules is a
145 # We need to wait until we have an input_api object and use this
146 # roundabout construct to import checkdeps because this file is
147 # eval-ed and thus doesn't have __file__.
148 original_sys_path = sys.path
150 sys.path = sys.path + [input_api.os_path.join(
151 input_api.PresubmitLocalPath(), 'buildtools', 'checkdeps')]
153 from cpp_checker import CppChecker
154 from rules import Rule
156 # Restore sys.path to what it was before.
157 sys.path = original_sys_path
159 def _FilesImpactedByDepsChange(files):
160 all_files = [f.AbsoluteLocalPath() for f in files]
161 deps_files = [p for p in all_files if IsDepsFile(p)]
162 impacted_files = union([_CollectImpactedFiles(path) for path in deps_files])
163 impacted_file_objs = [ImpactedFile(path) for path in impacted_files]
164 return impacted_file_objs
167 return os.path.isfile(p) and os.path.basename(p) == 'DEPS'
169 def union(list_of_lists):
170 """Ensure no duplicates"""
171 return set(sum(list_of_lists, []))
173 def _CollectImpactedFiles(deps_file):
174 # TODO(liviurau): Do not walk paths twice. Then we have no duplicates.
175 # Higher level DEPS changes may dominate lower level DEPS changes.
176 # TODO(liviurau): Check if DEPS changed in the right way.
177 # 'include_rules' impact c++ files but 'vars' or 'deps' do not.
178 # Maybe we just eval both old and new DEPS content and check
179 # if the list are the same.
181 parent_dir = os.path.dirname(deps_file)
182 for relative_f in input_api.change.AllFiles(parent_dir):
183 abs_f = os.path.join(parent_dir, relative_f)
184 if CppChecker.IsCppFile(abs_f):
188 class ImpactedFile(object):
189 """Duck type version of AffectedFile needed to check files under directories
190 where a DEPS file changed. Extend the interface along the line of
191 AffectedFile if you need it for other checks."""
193 def __init__(self, path):
197 path = self._path.replace(os.sep, '/')
198 return os.path.normpath(path)
200 def ChangedContents(self):
201 with open(self._path) as f:
202 # TODO(liviurau): read only '#include' lines
203 lines = f.readlines()
204 return enumerate(lines, start=1)
206 def _FilterDuplicates(impacted_files, affected_files):
207 """"We include all impacted files but exclude affected files that are also
208 impacted. Files impacted by DEPS changes take precedence before files
209 affected by direct changes."""
210 result = impacted_files[:]
211 only_paths = set([imf.LocalPath() for imf in impacted_files])
212 for af in affected_files:
213 if not af.LocalPath() in only_paths:
218 affected_files = input_api.AffectedFiles()
219 impacted_by_deps = _FilesImpactedByDepsChange(affected_files)
220 for f in _FilterDuplicates(impacted_by_deps, affected_files):
221 if not CppChecker.IsCppFile(f.LocalPath()):
224 changed_lines = [line for line_num, line in f.ChangedContents()]
225 added_includes.append([f.LocalPath(), changed_lines])
227 deps_checker = checkdeps.DepsChecker(input_api.PresubmitLocalPath())
229 error_descriptions = []
230 warning_descriptions = []
231 for path, rule_type, rule_description in deps_checker.CheckAddedCppIncludes(
233 description_with_path = '{}\n {}'.format(path, rule_description)
234 if rule_type == Rule.DISALLOW:
235 error_descriptions.append(description_with_path)
237 warning_descriptions.append(description_with_path)
240 if error_descriptions:
241 results.append(output_api.PresubmitError(
242 'You added one or more #includes that violate checkdeps rules.',
244 if warning_descriptions:
245 results.append(output_api.PresubmitPromptOrNotify(
246 'You added one or more #includes of files that are temporarily\n'
247 'allowed but being removed. Can you avoid introducing the\n'
248 '#include? See relevant DEPS file(s) for details and contacts.',
249 warning_descriptions))
253 def _CheckHeadersHaveIncludeGuards(input_api, output_api):
254 """Ensures that all header files have include guards."""
255 file_inclusion_pattern = r'src/.+\.h'
257 def FilterFile(affected_file):
258 files_to_skip = _EXCLUDED_PATHS + input_api.DEFAULT_FILES_TO_SKIP
259 return input_api.FilterSourceFile(
261 files_to_check=(file_inclusion_pattern, ),
262 files_to_skip=files_to_skip)
264 leading_src_pattern = input_api.re.compile(r'^src[\\\/]')
265 dash_dot_slash_pattern = input_api.re.compile(r'[-.\\\/]')
267 def PathToGuardMacro(path):
268 """Guards should be of the form V8_PATH_TO_FILE_WITHOUT_SRC_H_."""
269 x = input_api.re.sub(leading_src_pattern, 'v8_', path)
270 x = input_api.re.sub(dash_dot_slash_pattern, '_', x)
275 for f in input_api.AffectedSourceFiles(FilterFile):
276 local_path = f.LocalPath()
277 guard_macro = PathToGuardMacro(local_path)
279 input_api.re.compile(r'^#ifndef ' + guard_macro + '$'),
280 input_api.re.compile(r'^#define ' + guard_macro + '$'),
281 input_api.re.compile(r'^#endif // ' + guard_macro + '$')]
282 skip_check_pattern = input_api.re.compile(
283 r'^// PRESUBMIT_INTENTIONALLY_MISSING_INCLUDE_GUARD')
284 found_patterns = [ False, False, False ]
287 for line in f.NewContents():
288 for i in range(len(guard_patterns)):
289 if guard_patterns[i].match(line):
290 found_patterns[i] = True
291 if skip_check_pattern.match(line):
295 if not file_omitted and not all(found_patterns):
296 problems.append('{}: Missing include guard \'{}\''.format(
297 local_path, guard_macro))
300 return [output_api.PresubmitError(
301 'You added one or more header files without an appropriate\n'
302 'include guard. Add the include guard {#ifndef,#define,#endif}\n'
303 'triplet or omit the check entirely through the magic comment:\n'
304 '"// PRESUBMIT_INTENTIONALLY_MISSING_INCLUDE_GUARD".', problems)]
309 def _CheckNoInlineHeaderIncludesInNormalHeaders(input_api, output_api):
310 """Attempts to prevent inclusion of inline headers into normal header
311 files. This tries to establish a layering where inline headers can be
312 included by other inline headers or compilation units only."""
313 file_inclusion_pattern = r'(?!.+-inl\.h).+\.h'
314 include_directive_pattern = input_api.re.compile(r'#include ".+-inl.h"')
316 'You are including an inline header (e.g. foo-inl.h) within a normal\n'
317 'header (e.g. bar.h) file. This violates layering of dependencies.')
319 def FilterFile(affected_file):
320 files_to_skip = _EXCLUDED_PATHS + input_api.DEFAULT_FILES_TO_SKIP
321 return input_api.FilterSourceFile(
323 files_to_check=(file_inclusion_pattern, ),
324 files_to_skip=files_to_skip)
327 for f in input_api.AffectedSourceFiles(FilterFile):
328 local_path = f.LocalPath()
329 for line_number, line in f.ChangedContents():
330 if (include_directive_pattern.search(line)):
331 problems.append('{}:{}\n {}'.format(local_path, line_number,
335 return [output_api.PresubmitError(include_error, problems)]
340 def _CheckNoProductionCodeUsingTestOnlyFunctions(input_api, output_api):
341 """Attempts to prevent use of functions intended only for testing in
342 non-testing code. For now this is just a best-effort implementation
343 that ignores header files and may have some false positives. A
344 better implementation would probably need a proper C++ parser.
346 # We only scan .cc files, as the declaration of for-testing functions in
347 # header files are hard to distinguish from calls to such functions without a
349 file_inclusion_pattern = r'.+\.cc'
351 base_function_pattern = r'[ :]test::[^\s]+|ForTest(ing)?|for_test(ing)?'
352 inclusion_pattern = input_api.re.compile(
353 r'({})\s*\('.format(base_function_pattern))
354 comment_pattern = input_api.re.compile(
355 r'//.*({})'.format(base_function_pattern))
356 exclusion_pattern = input_api.re.compile(
357 r'::[A-Za-z0-9_]+({})|({})[^;]+'.format(base_function_pattern,
358 base_function_pattern) + '\{')
360 def FilterFile(affected_file):
361 files_to_skip = (_EXCLUDED_PATHS +
362 _TEST_CODE_EXCLUDED_PATHS +
363 input_api.DEFAULT_FILES_TO_SKIP)
364 return input_api.FilterSourceFile(
366 files_to_check=(file_inclusion_pattern, ),
367 files_to_skip=files_to_skip)
370 for f in input_api.AffectedSourceFiles(FilterFile):
371 local_path = f.LocalPath()
372 for line_number, line in f.ChangedContents():
373 if (inclusion_pattern.search(line) and
374 not comment_pattern.search(line) and
375 not exclusion_pattern.search(line)):
376 problems.append('{}:{}\n {}'.format(local_path, line_number,
380 return [output_api.PresubmitPromptOrNotify(_TEST_ONLY_WARNING, problems)]
385 def _CheckGenderNeutralInLicenses(input_api, output_api):
386 # License files are taken as is, even if they include gendered pronouns.
387 def LicenseFilter(path):
388 input_api.FilterSourceFile(path, files_to_skip=_LICENSE_FILE)
390 return input_api.canned_checks.CheckGenderNeutral(
391 input_api, output_api, source_file_filter=LicenseFilter)
394 def _RunTestsWithVPythonSpec(input_api, output_api):
395 return input_api.RunTests(
396 input_api.canned_checks.CheckVPythonSpec(input_api, output_api))
399 def _CommonChecks(input_api, output_api):
400 """Checks common to both upload and commit."""
401 # TODO(machenbach): Replace some of those checks, e.g. owners and copyright,
402 # with the canned PanProjectChecks. Need to make sure that the checks all
403 # pass on all existing files.
405 input_api.canned_checks.CheckOwnersFormat,
406 input_api.canned_checks.CheckOwners,
407 _CheckCommitMessageBugEntry,
408 input_api.canned_checks.CheckPatchFormatted,
409 _CheckGenderNeutralInLicenses,
411 _CheckUnwantedDependencies,
412 _CheckNoProductionCodeUsingTestOnlyFunctions,
413 _CheckHeadersHaveIncludeGuards,
414 _CheckNoInlineHeaderIncludesInNormalHeaders,
416 _CheckNoexceptAnnotations,
417 _RunTestsWithVPythonSpec,
420 return sum([check(input_api, output_api) for check in checks], [])
423 def _SkipTreeCheck(input_api, output_api):
424 """Check the env var whether we want to skip tree check.
425 Only skip if include/v8-version.h has been updated."""
426 src_version = 'include/v8-version.h'
427 if not input_api.AffectedSourceFiles(
428 lambda file: file.LocalPath() == src_version):
430 return input_api.environ.get('PRESUBMIT_TREE_CHECK') == 'skip'
433 def _CheckCommitMessageBugEntry(input_api, output_api):
434 """Check that bug entries are well-formed in commit message."""
436 'Bogus BUG entry: {}. Please specify prefix:number for v8 or chromium '
437 '(e.g. chromium:12345) or b/number for buganizer.')
439 for bug in (input_api.change.BUG or '').split(','):
441 if 'none'.startswith(bug.lower()):
443 if ':' not in bug and not bug.startswith('b/'):
445 if int(bug) > 10000000:
447 'Buganizer entry requires issue tracker prefix b/{}'.format(bug))
449 if int(bug) > 200000:
450 prefix_guess = 'chromium'
454 'BUG entry requires issue tracker prefix, e.g. {}:{}'.format(
457 results.append(bogus_bug_msg.format(bug))
458 elif not re.match(r'\w+[:\/]\d+', bug):
459 results.append(bogus_bug_msg.format(bug))
460 return [output_api.PresubmitError(r) for r in results]
463 def _CheckJSONFiles(input_api, output_api):
464 def FilterFile(affected_file):
465 return input_api.FilterSourceFile(
467 files_to_check=(r'.+\.json',))
470 for f in input_api.AffectedFiles(
471 file_filter=FilterFile, include_deletes=False):
472 with open(f.LocalPath()) as j:
475 except Exception as e:
476 results.append('JSON validation failed for {}. Error:\n{}'.format(
479 return [output_api.PresubmitError(r) for r in results]
482 def _CheckNoexceptAnnotations(input_api, output_api):
484 Checks that all user-defined constructors and assignment operators are marked
487 This is required for standard containers to pick the right constructors. Our
488 macros (like MOVE_ONLY_WITH_DEFAULT_CONSTRUCTORS) add this automatically.
489 Omitting it at some places can result in weird compiler errors if this is
490 mixed with other classes that have the annotation.
492 TODO(clemensb): This check should eventually be enabled for all files via
493 tools/presubmit.py (https://crbug.com/v8/8616).
496 def FilterFile(affected_file):
497 files_to_skip = _EXCLUDED_PATHS + (
498 # Skip api.cc since we cannot easily add the 'noexcept' annotation to
500 r'src[\\\/]api[\\\/]api\.cc',
501 # Skip src/bigint/ because it's meant to be V8-independent.
502 r'src[\\\/]bigint[\\\/].*',
504 return input_api.FilterSourceFile(
506 files_to_check=(r'src[\\\/].*\.cc', r'src[\\\/].*\.h',
507 r'test[\\\/].*\.cc', r'test[\\\/].*\.h'),
508 files_to_skip=files_to_skip)
510 # matches any class name.
511 class_name = r'\b([A-Z][A-Za-z0-9_:]*)(?:::\1)?'
512 # initial class name is potentially followed by this to declare an assignment
514 potential_assignment = r'(?:&\s+(?:\1::)?operator=)?\s*'
515 # matches an argument list that contains only a reference to a class named
516 # like the first capture group, potentially const.
517 single_class_ref_arg = r'\(\s*(?:const\s+)?\1(?:::\1)?&&?[^,;)]*\)'
518 # matches anything but a sequence of whitespaces followed by either
519 # V8_NOEXCEPT or "= delete".
520 not_followed_by_noexcept = r'(?!\s+(?:V8_NOEXCEPT|=\s+delete)\b)'
521 full_pattern = r'^.*?' + class_name + potential_assignment + \
522 single_class_ref_arg + not_followed_by_noexcept + '.*?$'
523 regexp = input_api.re.compile(full_pattern, re.MULTILINE)
526 for f in input_api.AffectedFiles(file_filter=FilterFile,
527 include_deletes=False):
528 with open(f.LocalPath()) as fh:
529 for match in re.finditer(regexp, fh.read()):
530 errors.append(f'in {f.LocalPath()}: {match.group().strip()}')
533 return [output_api.PresubmitPromptOrNotify(
534 'Copy constructors, move constructors, copy assignment operators and '
535 'move assignment operators should be marked V8_NOEXCEPT.\n'
536 'Please report false positives on https://crbug.com/v8/8616.',
541 def CheckChangeOnUpload(input_api, output_api):
543 results.extend(_CommonChecks(input_api, output_api))
547 def CheckChangeOnCommit(input_api, output_api):
549 results.extend(_CommonChecks(input_api, output_api))
550 results.extend(input_api.canned_checks.CheckChangeHasDescription(
551 input_api, output_api))
552 if not _SkipTreeCheck(input_api, output_api):
553 results.extend(input_api.canned_checks.CheckTreeIsOpen(
554 input_api, output_api,
555 json_url='http://v8-status.appspot.com/current?format=json'))