• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2019 The ANGLE Project Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4"""Top-level presubmit script for code generation.
5
6See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
7for more details on the presubmit API built into depot_tools.
8"""
9
10import os
11import re
12import shutil
13import subprocess
14import sys
15import tempfile
16
17# Fragment of a regular expression that matches C/C++ and Objective-C++ implementation files and headers.
18_IMPLEMENTATION_AND_HEADER_EXTENSIONS = r'\.(c|cc|cpp|cxx|mm|h|hpp|hxx)$'
19
20# Fragment of a regular expression that matches C++ and Objective-C++ header files.
21_HEADER_EXTENSIONS = r'\.(h|hpp|hxx)$'
22
23_PRIMARY_EXPORT_TARGETS = [
24    '//:libEGL',
25    '//:libGLESv1_CM',
26    '//:libGLESv2',
27    '//:translator',
28]
29
30
31def _CheckCommitMessageFormatting(input_api, output_api):
32
33    def _IsLineBlank(line):
34        return line.isspace() or line == ""
35
36    def _PopBlankLines(lines, reverse=False):
37        if reverse:
38            while len(lines) > 0 and _IsLineBlank(lines[-1]):
39                lines.pop()
40        else:
41            while len(lines) > 0 and _IsLineBlank(lines[0]):
42                lines.pop(0)
43
44    def _IsTagLine(line):
45        return ":" in line
46
47    def _SplitIntoMultipleCommits(description_text):
48        paragraph_split_pattern = r"((?m)^\s*$\n)"
49        multiple_paragraphs = re.split(paragraph_split_pattern, description_text)
50        multiple_commits = [""]
51        change_id_pattern = re.compile(r"(?m)^Change-Id: [a-zA-Z0-9]*$")
52        for paragraph in multiple_paragraphs:
53            multiple_commits[-1] += paragraph
54            if change_id_pattern.search(paragraph):
55                multiple_commits.append("")
56        if multiple_commits[-1] == "":
57            multiple_commits.pop()
58        return multiple_commits
59
60    def _CheckTabInCommit(lines):
61        return all([line.find("\t") == -1 for line in lines])
62
63    allowlist_strings = ['Revert "', 'Roll ', 'Reland ', 'Re-land ']
64    summary_linelength_warning_lower_limit = 65
65    summary_linelength_warning_upper_limit = 70
66    description_linelength_limit = 72
67
68    git_output = input_api.change.DescriptionText()
69
70    multiple_commits = _SplitIntoMultipleCommits(git_output)
71    errors = []
72
73    for k in range(len(multiple_commits)):
74        commit_msg_lines = multiple_commits[k].splitlines()
75        commit_number = len(multiple_commits) - k
76        commit_tag = "Commit " + str(commit_number) + ":"
77        commit_msg_line_numbers = {}
78        for i in range(len(commit_msg_lines)):
79            commit_msg_line_numbers[commit_msg_lines[i]] = i + 1
80        _PopBlankLines(commit_msg_lines, True)
81        _PopBlankLines(commit_msg_lines, False)
82        allowlisted = False
83        if len(commit_msg_lines) > 0:
84            for allowlist_string in allowlist_strings:
85                if commit_msg_lines[0].startswith(allowlist_string):
86                    allowlisted = True
87                    break
88        if allowlisted:
89            continue
90
91        if not _CheckTabInCommit(commit_msg_lines):
92            errors.append(
93                output_api.PresubmitError(commit_tag + "Tabs are not allowed in commit message."))
94
95        # the tags paragraph is at the end of the message
96        # the break between the tags paragraph is the first line without ":"
97        # this is sufficient because if a line is blank, it will not have ":"
98        last_paragraph_line_count = 0
99        while len(commit_msg_lines) > 0 and _IsTagLine(commit_msg_lines[-1]):
100            last_paragraph_line_count += 1
101            commit_msg_lines.pop()
102        if last_paragraph_line_count == 0:
103            errors.append(
104                output_api.PresubmitError(
105                    commit_tag +
106                    "Please ensure that there are tags (e.g., Bug:, Test:) in your description."))
107        if len(commit_msg_lines) > 0:
108            if not _IsLineBlank(commit_msg_lines[-1]):
109                output_api.PresubmitError(commit_tag +
110                                          "Please ensure that there exists 1 blank line " +
111                                          "between tags and description body.")
112            else:
113                # pop the blank line between tag paragraph and description body
114                commit_msg_lines.pop()
115                if len(commit_msg_lines) > 0 and _IsLineBlank(commit_msg_lines[-1]):
116                    errors.append(
117                        output_api.PresubmitError(
118                            commit_tag + 'Please ensure that there exists only 1 blank line '
119                            'between tags and description body.'))
120                    # pop all the remaining blank lines between tag and description body
121                    _PopBlankLines(commit_msg_lines, True)
122        if len(commit_msg_lines) == 0:
123            errors.append(
124                output_api.PresubmitError(commit_tag +
125                                          'Please ensure that your description summary'
126                                          ' and description body are not blank.'))
127            continue
128
129        if summary_linelength_warning_lower_limit <= len(commit_msg_lines[0]) \
130        <= summary_linelength_warning_upper_limit:
131            errors.append(
132                output_api.PresubmitPromptWarning(
133                    commit_tag + "Your description summary should be on one line of " +
134                    str(summary_linelength_warning_lower_limit - 1) + " or less characters."))
135        elif len(commit_msg_lines[0]) > summary_linelength_warning_upper_limit:
136            errors.append(
137                output_api.PresubmitError(
138                    commit_tag + "Please ensure that your description summary is on one line of " +
139                    str(summary_linelength_warning_lower_limit - 1) + " or less characters."))
140        commit_msg_lines.pop(0)  # get rid of description summary
141        if len(commit_msg_lines) == 0:
142            continue
143        if not _IsLineBlank(commit_msg_lines[0]):
144            errors.append(
145                output_api.PresubmitError(commit_tag +
146                                          'Please ensure the summary is only 1 line and '
147                                          'there is 1 blank line between the summary '
148                                          'and description body.'))
149        else:
150            commit_msg_lines.pop(0)  # pop first blank line
151            if len(commit_msg_lines) == 0:
152                continue
153            if _IsLineBlank(commit_msg_lines[0]):
154                errors.append(
155                    output_api.PresubmitError(commit_tag +
156                                              'Please ensure that there exists only 1 blank line '
157                                              'between description summary and description body.'))
158                # pop all the remaining blank lines between
159                # description summary and description body
160                _PopBlankLines(commit_msg_lines)
161
162        # loop through description body
163        while len(commit_msg_lines) > 0:
164            line = commit_msg_lines.pop(0)
165            # lines starting with 4 spaces or lines without space(urls)
166            # are exempt from length check
167            if line.startswith("    ") or " " not in line:
168                continue
169            if len(line) > description_linelength_limit:
170                errors.append(
171                    output_api.PresubmitError(
172                        commit_tag + 'Line ' + str(commit_msg_line_numbers[line]) +
173                        ' is too long.\n' + '"' + line + '"\n' + 'Please wrap it to ' +
174                        str(description_linelength_limit) + ' characters. ' +
175                        "Lines without spaces or lines starting with 4 spaces are exempt."))
176                break
177    return errors
178
179
180def _CheckChangeHasBugField(input_api, output_api):
181    """Requires that the changelist have a Bug: field from a known project."""
182    bugs = input_api.change.BugsFromDescription()
183    if not bugs:
184        return [
185            output_api.PresubmitError('Please ensure that your description contains:\n'
186                                      '"Bug: angleproject:[bug number]"\n'
187                                      'directly above the Change-Id tag.')
188        ]
189
190    # The bug must be in the form of "project:number".  None is also accepted, which is used by
191    # rollers as well as in very minor changes.
192    if len(bugs) == 1 and bugs[0] == 'None':
193        return []
194
195    projects = [
196        'angleproject:', 'chromium:', 'dawn:', 'fuchsia:', 'skia:', 'swiftshader:', 'tint:', 'b/'
197    ]
198    bug_regex = re.compile(r"([a-z]+[:/])(\d+)")
199    errors = []
200    extra_help = None
201
202    for bug in bugs:
203        if bug == 'None':
204            errors.append(
205                output_api.PresubmitError('Invalid bug tag "None" in presence of other bug tags.'))
206            continue
207
208        match = re.match(bug_regex, bug)
209        if match == None or bug != match.group(0) or match.group(1) not in projects:
210            errors.append(output_api.PresubmitError('Incorrect bug tag "' + bug + '".'))
211            if not extra_help:
212                extra_help = output_api.PresubmitError('Acceptable format is:\n\n'
213                                                       '    Bug: project:bugnumber\n\n'
214                                                       'Acceptable projects are:\n\n    ' +
215                                                       '\n    '.join(projects))
216
217    if extra_help:
218        errors.append(extra_help)
219
220    return errors
221
222
223def _CheckCodeGeneration(input_api, output_api):
224
225    class Msg(output_api.PresubmitError):
226        """Specialized error message"""
227
228        def __init__(self, message):
229            super(output_api.PresubmitError, self).__init__(
230                message,
231                long_text='Please ensure your ANGLE repositiory is synced to tip-of-tree\n'
232                'and all ANGLE DEPS are fully up-to-date by running gclient sync.\n'
233                '\n'
234                'If that fails, run scripts/run_code_generation.py to refresh generated hashes.\n'
235                '\n'
236                'If you are building ANGLE inside Chromium you must bootstrap ANGLE\n'
237                'before gclient sync. See the DevSetup documentation for more details.\n')
238
239    code_gen_path = input_api.os_path.join(input_api.PresubmitLocalPath(),
240                                           'scripts/run_code_generation.py')
241    cmd_name = 'run_code_generation'
242    cmd = [input_api.python_executable, code_gen_path, '--verify-no-dirty']
243    test_cmd = input_api.Command(name=cmd_name, cmd=cmd, kwargs={}, message=Msg)
244    if input_api.verbose:
245        print('Running ' + cmd_name)
246    return input_api.RunTests([test_cmd])
247
248
249# Taken directly from Chromium's PRESUBMIT.py
250def _CheckNewHeaderWithoutGnChange(input_api, output_api):
251    """Checks that newly added header files have corresponding GN changes.
252  Note that this is only a heuristic. To be precise, run script:
253  build/check_gn_headers.py.
254  """
255
256    def headers(f):
257        return input_api.FilterSourceFile(f, files_to_check=(r'.+%s' % _HEADER_EXTENSIONS,))
258
259    new_headers = []
260    for f in input_api.AffectedSourceFiles(headers):
261        if f.Action() != 'A':
262            continue
263        new_headers.append(f.LocalPath())
264
265    def gn_files(f):
266        return input_api.FilterSourceFile(f, files_to_check=(r'.+\.gn',))
267
268    all_gn_changed_contents = ''
269    for f in input_api.AffectedSourceFiles(gn_files):
270        for _, line in f.ChangedContents():
271            all_gn_changed_contents += line
272
273    problems = []
274    for header in new_headers:
275        basename = input_api.os_path.basename(header)
276        if basename not in all_gn_changed_contents:
277            problems.append(header)
278
279    if problems:
280        return [
281            output_api.PresubmitPromptWarning(
282                'Missing GN changes for new header files',
283                items=sorted(problems),
284                long_text='Please double check whether newly added header files need '
285                'corresponding changes in gn or gni files.\nThis checking is only a '
286                'heuristic. Run build/check_gn_headers.py to be precise.\n'
287                'Read https://crbug.com/661774 for more info.')
288        ]
289    return []
290
291
292def _CheckExportValidity(input_api, output_api):
293    outdir = tempfile.mkdtemp()
294    # shell=True is necessary on Windows, as otherwise subprocess fails to find
295    # either 'gn' or 'vpython3' even if they are findable via PATH.
296    use_shell = input_api.is_windows
297    try:
298        try:
299            subprocess.check_output(['gn', 'gen', outdir], shell=use_shell)
300        except subprocess.CalledProcessError as e:
301            return [
302                output_api.PresubmitError(
303                    'Unable to run gn gen for export_targets.py: %s' % e.output)
304            ]
305        export_target_script = os.path.join(input_api.PresubmitLocalPath(), 'scripts',
306                                            'export_targets.py')
307        try:
308            subprocess.check_output(
309                ['vpython3', export_target_script, outdir] + _PRIMARY_EXPORT_TARGETS,
310                stderr=subprocess.STDOUT,
311                shell=use_shell)
312        except subprocess.CalledProcessError as e:
313            if input_api.is_committing:
314                return [output_api.PresubmitError('export_targets.py failed: %s' % e.output)]
315            return [
316                output_api.PresubmitPromptWarning(
317                    'export_targets.py failed, this may just be due to your local checkout: %s' %
318                    e.output)
319            ]
320        return []
321    finally:
322        shutil.rmtree(outdir)
323
324
325def _CheckTabsInSourceFiles(input_api, output_api):
326    """Forbids tab characters in source files due to a WebKit repo requirement. """
327
328    def implementation_and_headers_including_third_party(f):
329        # Check third_party files too, because WebKit's checks don't make exceptions.
330        return input_api.FilterSourceFile(
331            f,
332            files_to_check=(r'.+%s' % _IMPLEMENTATION_AND_HEADER_EXTENSIONS,),
333            files_to_skip=[f for f in input_api.DEFAULT_FILES_TO_SKIP if not "third_party" in f])
334
335    files_with_tabs = []
336    for f in input_api.AffectedSourceFiles(implementation_and_headers_including_third_party):
337        for (num, line) in f.ChangedContents():
338            if '\t' in line:
339                files_with_tabs.append(f)
340                break
341
342    if files_with_tabs:
343        return [
344            output_api.PresubmitError(
345                'Tab characters in source files.',
346                items=sorted(files_with_tabs),
347                long_text=
348                'Tab characters are forbidden in ANGLE source files because WebKit\'s Subversion\n'
349                'repository does not allow tab characters in source files.\n'
350                'Please remove tab characters from these files.')
351        ]
352    return []
353
354
355# https://stackoverflow.com/a/196392
356def is_ascii(s):
357    return all(ord(c) < 128 for c in s)
358
359
360def _CheckNonAsciiInSourceFiles(input_api, output_api):
361    """Forbids non-ascii characters in source files. """
362
363    def implementation_and_headers(f):
364        return input_api.FilterSourceFile(
365            f, files_to_check=(r'.+%s' % _IMPLEMENTATION_AND_HEADER_EXTENSIONS,))
366
367    files_with_non_ascii = []
368    for f in input_api.AffectedSourceFiles(implementation_and_headers):
369        for (num, line) in f.ChangedContents():
370            if not is_ascii(line):
371                files_with_non_ascii.append("%s: %s" % (f, line))
372                break
373
374    if files_with_non_ascii:
375        return [
376            output_api.PresubmitError(
377                'Non-ASCII characters in source files.',
378                items=sorted(files_with_non_ascii),
379                long_text='Non-ASCII characters are forbidden in ANGLE source files.\n'
380                'Please remove non-ASCII characters from these files.')
381        ]
382    return []
383
384
385def CheckChangeOnUpload(input_api, output_api):
386    results = []
387    results.extend(_CheckTabsInSourceFiles(input_api, output_api))
388    results.extend(_CheckNonAsciiInSourceFiles(input_api, output_api))
389    results.extend(_CheckCodeGeneration(input_api, output_api))
390    results.extend(_CheckChangeHasBugField(input_api, output_api))
391    results.extend(input_api.canned_checks.CheckChangeHasDescription(input_api, output_api))
392    results.extend(_CheckNewHeaderWithoutGnChange(input_api, output_api))
393    results.extend(_CheckExportValidity(input_api, output_api))
394    results.extend(
395        input_api.canned_checks.CheckPatchFormatted(
396            input_api, output_api, result_factory=output_api.PresubmitError))
397    results.extend(_CheckCommitMessageFormatting(input_api, output_api))
398    return results
399
400
401def CheckChangeOnCommit(input_api, output_api):
402    return CheckChangeOnUpload(input_api, output_api)
403