• 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
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