• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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