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