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 sys
35
36
37_EXCLUDED_PATHS = (
38    r"^test[\\\/].*",
39    r"^testing[\\\/].*",
40    r"^third_party[\\\/].*",
41    r"^tools[\\\/].*",
42)
43
44
45# Regular expression that matches code only used for test binaries
46# (best effort).
47_TEST_CODE_EXCLUDED_PATHS = (
48    r'.+-unittest\.cc',
49    # Has a method VisitForTest().
50    r'src[\\\/]compiler[\\\/]ast-graph-builder\.cc',
51    # Test extension.
52    r'src[\\\/]extensions[\\\/]gc-extension\.cc',
53)
54
55
56_TEST_ONLY_WARNING = (
57    'You might be calling functions intended only for testing from\n'
58    'production code.  It is OK to ignore this warning if you know what\n'
59    'you are doing, as the heuristics used to detect the situation are\n'
60    'not perfect.  The commit queue will not block on this warning.')
61
62
63def _V8PresubmitChecks(input_api, output_api):
64  """Runs the V8 presubmit checks."""
65  import sys
66  sys.path.append(input_api.os_path.join(
67        input_api.PresubmitLocalPath(), 'tools'))
68  from presubmit import CppLintProcessor
69  from presubmit import SourceProcessor
70  from presubmit import CheckExternalReferenceRegistration
71  from presubmit import CheckAuthorizedAuthor
72  from presubmit import CheckStatusFiles
73
74  results = []
75  if not CppLintProcessor().Run(input_api.PresubmitLocalPath()):
76    results.append(output_api.PresubmitError("C++ lint check failed"))
77  if not SourceProcessor().Run(input_api.PresubmitLocalPath()):
78    results.append(output_api.PresubmitError(
79        "Copyright header, trailing whitespaces and two empty lines " \
80        "between declarations check failed"))
81  if not CheckExternalReferenceRegistration(input_api.PresubmitLocalPath()):
82    results.append(output_api.PresubmitError(
83        "External references registration check failed"))
84  if not CheckStatusFiles(input_api.PresubmitLocalPath()):
85    results.append(output_api.PresubmitError("Status file check failed"))
86  results.extend(CheckAuthorizedAuthor(input_api, output_api))
87  return results
88
89
90def _CheckUnwantedDependencies(input_api, output_api):
91  """Runs checkdeps on #include statements added in this
92  change. Breaking - rules is an error, breaking ! rules is a
93  warning.
94  """
95  # We need to wait until we have an input_api object and use this
96  # roundabout construct to import checkdeps because this file is
97  # eval-ed and thus doesn't have __file__.
98  original_sys_path = sys.path
99  try:
100    sys.path = sys.path + [input_api.os_path.join(
101        input_api.PresubmitLocalPath(), 'buildtools', 'checkdeps')]
102    import checkdeps
103    from cpp_checker import CppChecker
104    from rules import Rule
105  finally:
106    # Restore sys.path to what it was before.
107    sys.path = original_sys_path
108
109  added_includes = []
110  for f in input_api.AffectedFiles():
111    if not CppChecker.IsCppFile(f.LocalPath()):
112      continue
113
114    changed_lines = [line for line_num, line in f.ChangedContents()]
115    added_includes.append([f.LocalPath(), changed_lines])
116
117  deps_checker = checkdeps.DepsChecker(input_api.PresubmitLocalPath())
118
119  error_descriptions = []
120  warning_descriptions = []
121  for path, rule_type, rule_description in deps_checker.CheckAddedCppIncludes(
122      added_includes):
123    description_with_path = '%s\n    %s' % (path, rule_description)
124    if rule_type == Rule.DISALLOW:
125      error_descriptions.append(description_with_path)
126    else:
127      warning_descriptions.append(description_with_path)
128
129  results = []
130  if error_descriptions:
131    results.append(output_api.PresubmitError(
132        'You added one or more #includes that violate checkdeps rules.',
133        error_descriptions))
134  if warning_descriptions:
135    results.append(output_api.PresubmitPromptOrNotify(
136        'You added one or more #includes of files that are temporarily\n'
137        'allowed but being removed. Can you avoid introducing the\n'
138        '#include? See relevant DEPS file(s) for details and contacts.',
139        warning_descriptions))
140  return results
141
142
143def _CheckNoInlineHeaderIncludesInNormalHeaders(input_api, output_api):
144  """Attempts to prevent inclusion of inline headers into normal header
145  files. This tries to establish a layering where inline headers can be
146  included by other inline headers or compilation units only."""
147  file_inclusion_pattern = r'(?!.+-inl\.h).+\.h'
148  include_directive_pattern = input_api.re.compile(r'#include ".+-inl.h"')
149  include_warning = (
150    'You might be including an inline header (e.g. foo-inl.h) within a\n'
151    'normal header (e.g. bar.h) file.  Can you avoid introducing the\n'
152    '#include?  The commit queue will not block on this warning.')
153
154  def FilterFile(affected_file):
155    black_list = (_EXCLUDED_PATHS +
156                  input_api.DEFAULT_BLACK_LIST)
157    return input_api.FilterSourceFile(
158      affected_file,
159      white_list=(file_inclusion_pattern, ),
160      black_list=black_list)
161
162  problems = []
163  for f in input_api.AffectedSourceFiles(FilterFile):
164    local_path = f.LocalPath()
165    for line_number, line in f.ChangedContents():
166      if (include_directive_pattern.search(line)):
167        problems.append(
168          '%s:%d\n    %s' % (local_path, line_number, line.strip()))
169
170  if problems:
171    return [output_api.PresubmitPromptOrNotify(include_warning, problems)]
172  else:
173    return []
174
175
176def _CheckNoProductionCodeUsingTestOnlyFunctions(input_api, output_api):
177  """Attempts to prevent use of functions intended only for testing in
178  non-testing code. For now this is just a best-effort implementation
179  that ignores header files and may have some false positives. A
180  better implementation would probably need a proper C++ parser.
181  """
182  # We only scan .cc files, as the declaration of for-testing functions in
183  # header files are hard to distinguish from calls to such functions without a
184  # proper C++ parser.
185  file_inclusion_pattern = r'.+\.cc'
186
187  base_function_pattern = r'[ :]test::[^\s]+|ForTest(ing)?|for_test(ing)?'
188  inclusion_pattern = input_api.re.compile(r'(%s)\s*\(' % base_function_pattern)
189  comment_pattern = input_api.re.compile(r'//.*(%s)' % base_function_pattern)
190  exclusion_pattern = input_api.re.compile(
191    r'::[A-Za-z0-9_]+(%s)|(%s)[^;]+\{' % (
192      base_function_pattern, base_function_pattern))
193
194  def FilterFile(affected_file):
195    black_list = (_EXCLUDED_PATHS +
196                  _TEST_CODE_EXCLUDED_PATHS +
197                  input_api.DEFAULT_BLACK_LIST)
198    return input_api.FilterSourceFile(
199      affected_file,
200      white_list=(file_inclusion_pattern, ),
201      black_list=black_list)
202
203  problems = []
204  for f in input_api.AffectedSourceFiles(FilterFile):
205    local_path = f.LocalPath()
206    for line_number, line in f.ChangedContents():
207      if (inclusion_pattern.search(line) and
208          not comment_pattern.search(line) and
209          not exclusion_pattern.search(line)):
210        problems.append(
211          '%s:%d\n    %s' % (local_path, line_number, line.strip()))
212
213  if problems:
214    return [output_api.PresubmitPromptOrNotify(_TEST_ONLY_WARNING, problems)]
215  else:
216    return []
217
218
219def _CommonChecks(input_api, output_api):
220  """Checks common to both upload and commit."""
221  results = []
222  results.extend(input_api.canned_checks.CheckOwners(
223      input_api, output_api, source_file_filter=None))
224  results.extend(input_api.canned_checks.CheckPatchFormatted(
225      input_api, output_api))
226  results.extend(_V8PresubmitChecks(input_api, output_api))
227  results.extend(_CheckUnwantedDependencies(input_api, output_api))
228  results.extend(
229      _CheckNoProductionCodeUsingTestOnlyFunctions(input_api, output_api))
230  results.extend(
231      _CheckNoInlineHeaderIncludesInNormalHeaders(input_api, output_api))
232  return results
233
234
235def _SkipTreeCheck(input_api, output_api):
236  """Check the env var whether we want to skip tree check.
237     Only skip if include/v8-version.h has been updated."""
238  src_version = 'include/v8-version.h'
239  if not input_api.AffectedSourceFiles(
240      lambda file: file.LocalPath() == src_version):
241    return False
242  return input_api.environ.get('PRESUBMIT_TREE_CHECK') == 'skip'
243
244
245def _CheckChangeLogFlag(input_api, output_api, warn):
246  """Checks usage of LOG= flag in the commit message."""
247  results = []
248  if (input_api.change.BUG and input_api.change.BUG != 'none' and
249      not 'LOG' in input_api.change.tags):
250    text = ('An issue reference (BUG=) requires a change log flag (LOG=). '
251            'Use LOG=Y for including this commit message in the change log. '
252            'Use LOG=N or leave blank otherwise.')
253    if warn:
254      results.append(output_api.PresubmitPromptWarning(text))
255    else:
256      results.append(output_api.PresubmitError(text))
257  return results
258
259
260def CheckChangeOnUpload(input_api, output_api):
261  results = []
262  results.extend(_CommonChecks(input_api, output_api))
263  results.extend(_CheckChangeLogFlag(input_api, output_api, True))
264  return results
265
266
267def CheckChangeOnCommit(input_api, output_api):
268  results = []
269  results.extend(_CommonChecks(input_api, output_api))
270  results.extend(_CheckChangeLogFlag(input_api, output_api, False))
271  results.extend(input_api.canned_checks.CheckChangeHasDescription(
272      input_api, output_api))
273  if not _SkipTreeCheck(input_api, output_api):
274    results.extend(input_api.canned_checks.CheckTreeIsOpen(
275        input_api, output_api,
276        json_url='http://v8-status.appspot.com/current?format=json'))
277  return results
278