Revert "Upgrade NodeJS binary to v16.13.0"
[platform/framework/web/chromium-efl.git] / third_party / angle / PRESUBMIT.py
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
6 See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
7 for more details on the presubmit API built into depot_tools.
8 """
9
10 import itertools
11 import os
12 import re
13 import shutil
14 import subprocess
15 import sys
16 import tempfile
17 import textwrap
18 import pathlib
19
20 # This line is 'magic' in that git-cl looks for it to decide whether to
21 # use Python3 instead of Python2 when running the code in this file.
22 USE_PYTHON3 = True
23
24 # Fragment of a regular expression that matches C/C++ and Objective-C++ implementation files and headers.
25 _IMPLEMENTATION_AND_HEADER_EXTENSIONS = r'\.(c|cc|cpp|cxx|mm|h|hpp|hxx)$'
26
27 # Fragment of a regular expression that matches C++ and Objective-C++ header files.
28 _HEADER_EXTENSIONS = r'\.(h|hpp|hxx)$'
29
30 _PRIMARY_EXPORT_TARGETS = [
31     '//:libEGL',
32     '//:libGLESv1_CM',
33     '//:libGLESv2',
34     '//:translator',
35 ]
36
37
38 def _SplitIntoMultipleCommits(description_text):
39     paragraph_split_pattern = r"(?m)(^\s*$\n)"
40     multiple_paragraphs = re.split(paragraph_split_pattern, description_text)
41     multiple_commits = [""]
42     change_id_pattern = re.compile(r"(?m)^Change-Id: [a-zA-Z0-9]*$")
43     for paragraph in multiple_paragraphs:
44         multiple_commits[-1] += paragraph
45         if change_id_pattern.search(paragraph):
46             multiple_commits.append("")
47     if multiple_commits[-1] == "":
48         multiple_commits.pop()
49     return multiple_commits
50
51
52 def _CheckCommitMessageFormatting(input_api, output_api):
53
54     def _IsLineBlank(line):
55         return line.isspace() or line == ""
56
57     def _PopBlankLines(lines, reverse=False):
58         if reverse:
59             while len(lines) > 0 and _IsLineBlank(lines[-1]):
60                 lines.pop()
61         else:
62             while len(lines) > 0 and _IsLineBlank(lines[0]):
63                 lines.pop(0)
64
65     def _IsTagLine(line):
66         return ":" in line
67
68     def _CheckTabInCommit(lines):
69         return all([line.find("\t") == -1 for line in lines])
70
71     allowlist_strings = ['Revert', 'Roll', 'Manual roll', 'Reland', 'Re-land']
72     summary_linelength_warning_lower_limit = 65
73     summary_linelength_warning_upper_limit = 70
74     description_linelength_limit = 72
75
76     git_output = input_api.change.DescriptionText()
77
78     multiple_commits = _SplitIntoMultipleCommits(git_output)
79     errors = []
80
81     for k in range(len(multiple_commits)):
82         commit_msg_lines = multiple_commits[k].splitlines()
83         commit_number = len(multiple_commits) - k
84         commit_tag = "Commit " + str(commit_number) + ":"
85         commit_msg_line_numbers = {}
86         for i in range(len(commit_msg_lines)):
87             commit_msg_line_numbers[commit_msg_lines[i]] = i + 1
88         _PopBlankLines(commit_msg_lines, True)
89         _PopBlankLines(commit_msg_lines, False)
90         allowlisted = False
91         if len(commit_msg_lines) > 0:
92             for allowlist_string in allowlist_strings:
93                 if commit_msg_lines[0].startswith(allowlist_string):
94                     allowlisted = True
95                     break
96         if allowlisted:
97             continue
98
99         if not _CheckTabInCommit(commit_msg_lines):
100             errors.append(
101                 output_api.PresubmitError(commit_tag + "Tabs are not allowed in commit message."))
102
103         # the tags paragraph is at the end of the message
104         # the break between the tags paragraph is the first line without ":"
105         # this is sufficient because if a line is blank, it will not have ":"
106         last_paragraph_line_count = 0
107         while len(commit_msg_lines) > 0 and _IsTagLine(commit_msg_lines[-1]):
108             last_paragraph_line_count += 1
109             commit_msg_lines.pop()
110         if last_paragraph_line_count == 0:
111             errors.append(
112                 output_api.PresubmitError(
113                     commit_tag +
114                     "Please ensure that there are tags (e.g., Bug:, Test:) in your description."))
115         if len(commit_msg_lines) > 0:
116             if not _IsLineBlank(commit_msg_lines[-1]):
117                 output_api.PresubmitError(commit_tag +
118                                           "Please ensure that there exists 1 blank line " +
119                                           "between tags and description body.")
120             else:
121                 # pop the blank line between tag paragraph and description body
122                 commit_msg_lines.pop()
123                 if len(commit_msg_lines) > 0 and _IsLineBlank(commit_msg_lines[-1]):
124                     errors.append(
125                         output_api.PresubmitError(
126                             commit_tag + 'Please ensure that there exists only 1 blank line '
127                             'between tags and description body.'))
128                     # pop all the remaining blank lines between tag and description body
129                     _PopBlankLines(commit_msg_lines, True)
130         if len(commit_msg_lines) == 0:
131             errors.append(
132                 output_api.PresubmitError(commit_tag +
133                                           'Please ensure that your description summary'
134                                           ' and description body are not blank.'))
135             continue
136
137         if summary_linelength_warning_lower_limit <= len(commit_msg_lines[0]) \
138         <= summary_linelength_warning_upper_limit:
139             errors.append(
140                 output_api.PresubmitPromptWarning(
141                     commit_tag + "Your description summary should be on one line of " +
142                     str(summary_linelength_warning_lower_limit - 1) + " or less characters."))
143         elif len(commit_msg_lines[0]) > summary_linelength_warning_upper_limit:
144             errors.append(
145                 output_api.PresubmitError(
146                     commit_tag + "Please ensure that your description summary is on one line of " +
147                     str(summary_linelength_warning_lower_limit - 1) + " or less characters."))
148         commit_msg_lines.pop(0)  # get rid of description summary
149         if len(commit_msg_lines) == 0:
150             continue
151         if not _IsLineBlank(commit_msg_lines[0]):
152             errors.append(
153                 output_api.PresubmitError(commit_tag +
154                                           'Please ensure the summary is only 1 line and '
155                                           'there is 1 blank line between the summary '
156                                           'and description body.'))
157         else:
158             commit_msg_lines.pop(0)  # pop first blank line
159             if len(commit_msg_lines) == 0:
160                 continue
161             if _IsLineBlank(commit_msg_lines[0]):
162                 errors.append(
163                     output_api.PresubmitError(commit_tag +
164                                               'Please ensure that there exists only 1 blank line '
165                                               'between description summary and description body.'))
166                 # pop all the remaining blank lines between
167                 # description summary and description body
168                 _PopBlankLines(commit_msg_lines)
169
170         # loop through description body
171         while len(commit_msg_lines) > 0:
172             line = commit_msg_lines.pop(0)
173             # lines starting with 4 spaces, quotes or lines without space(urls)
174             # are exempt from length check
175             if line.startswith("    ") or line.startswith("> ") or " " not in line:
176                 continue
177             if len(line) > description_linelength_limit:
178                 errors.append(
179                     output_api.PresubmitError(
180                         commit_tag + 'Line ' + str(commit_msg_line_numbers[line]) +
181                         ' is too long.\n' + '"' + line + '"\n' + 'Please wrap it to ' +
182                         str(description_linelength_limit) + ' characters. ' +
183                         "Lines without spaces or lines starting with 4 spaces are exempt."))
184                 break
185     return errors
186
187
188 def _CheckChangeHasBugField(input_api, output_api):
189     """Requires that the changelist have a Bug: field from a known project."""
190     bugs = input_api.change.BugsFromDescription()
191     if not bugs:
192         return [
193             output_api.PresubmitError('Please ensure that your description contains:\n'
194                                       '"Bug: angleproject:[bug number]"\n'
195                                       'directly above the Change-Id tag.')
196         ]
197
198     # The bug must be in the form of "project:number".  None is also accepted, which is used by
199     # rollers as well as in very minor changes.
200     if len(bugs) == 1 and bugs[0] == 'None':
201         return []
202
203     projects = [
204         'angleproject:', 'chromium:', 'dawn:', 'fuchsia:', 'skia:', 'swiftshader:', 'tint:', 'b/'
205     ]
206     bug_regex = re.compile(r"([a-z]+[:/])(\d+)")
207     errors = []
208     extra_help = None
209
210     for bug in bugs:
211         if bug == 'None':
212             errors.append(
213                 output_api.PresubmitError('Invalid bug tag "None" in presence of other bug tags.'))
214             continue
215
216         match = re.match(bug_regex, bug)
217         if match == None or bug != match.group(0) or match.group(1) not in projects:
218             errors.append(output_api.PresubmitError('Incorrect bug tag "' + bug + '".'))
219             if not extra_help:
220                 extra_help = output_api.PresubmitError('Acceptable format is:\n\n'
221                                                        '    Bug: project:bugnumber\n\n'
222                                                        'Acceptable projects are:\n\n    ' +
223                                                        '\n    '.join(projects))
224
225     if extra_help:
226         errors.append(extra_help)
227
228     return errors
229
230
231 def _CheckCodeGeneration(input_api, output_api):
232
233     class Msg(output_api.PresubmitError):
234         """Specialized error message"""
235
236         def __init__(self, message):
237             super(output_api.PresubmitError, self).__init__(
238                 message,
239                 long_text='Please ensure your ANGLE repositiory is synced to tip-of-tree\n'
240                 'and all ANGLE DEPS are fully up-to-date by running gclient sync.\n'
241                 '\n'
242                 'If that fails, run scripts/run_code_generation.py to refresh generated hashes.\n'
243                 '\n'
244                 'If you are building ANGLE inside Chromium you must bootstrap ANGLE\n'
245                 'before gclient sync. See the DevSetup documentation for more details.\n')
246
247     code_gen_path = input_api.os_path.join(input_api.PresubmitLocalPath(),
248                                            'scripts/run_code_generation.py')
249     cmd_name = 'run_code_generation'
250     cmd = [input_api.python3_executable, code_gen_path, '--verify-no-dirty']
251     test_cmd = input_api.Command(name=cmd_name, cmd=cmd, kwargs={}, message=Msg)
252     if input_api.verbose:
253         print('Running ' + cmd_name)
254     return input_api.RunTests([test_cmd])
255
256
257 # Taken directly from Chromium's PRESUBMIT.py
258 def _CheckNewHeaderWithoutGnChange(input_api, output_api):
259     """Checks that newly added header files have corresponding GN changes.
260   Note that this is only a heuristic. To be precise, run script:
261   build/check_gn_headers.py.
262   """
263
264     def headers(f):
265         return input_api.FilterSourceFile(f, files_to_check=(r'.+%s' % _HEADER_EXTENSIONS,))
266
267     new_headers = []
268     for f in input_api.AffectedSourceFiles(headers):
269         if f.Action() != 'A':
270             continue
271         new_headers.append(f.LocalPath())
272
273     def gn_files(f):
274         return input_api.FilterSourceFile(f, files_to_check=(r'.+\.gn',))
275
276     all_gn_changed_contents = ''
277     for f in input_api.AffectedSourceFiles(gn_files):
278         for _, line in f.ChangedContents():
279             all_gn_changed_contents += line
280
281     problems = []
282     for header in new_headers:
283         basename = input_api.os_path.basename(header)
284         if basename not in all_gn_changed_contents:
285             problems.append(header)
286
287     if problems:
288         return [
289             output_api.PresubmitPromptWarning(
290                 'Missing GN changes for new header files',
291                 items=sorted(problems),
292                 long_text='Please double check whether newly added header files need '
293                 'corresponding changes in gn or gni files.\nThis checking is only a '
294                 'heuristic. Run build/check_gn_headers.py to be precise.\n'
295                 'Read https://crbug.com/661774 for more info.')
296         ]
297     return []
298
299
300 def _CheckExportValidity(input_api, output_api):
301     outdir = tempfile.mkdtemp()
302     # shell=True is necessary on Windows, as otherwise subprocess fails to find
303     # either 'gn' or 'vpython3' even if they are findable via PATH.
304     use_shell = input_api.is_windows
305     try:
306         try:
307             subprocess.check_output(['gn', 'gen', outdir], shell=use_shell)
308         except subprocess.CalledProcessError as e:
309             return [
310                 output_api.PresubmitError(
311                     'Unable to run gn gen for export_targets.py: %s' % e.output)
312             ]
313         export_target_script = os.path.join(input_api.PresubmitLocalPath(), 'scripts',
314                                             'export_targets.py')
315         try:
316             subprocess.check_output(
317                 ['vpython3', export_target_script, outdir] + _PRIMARY_EXPORT_TARGETS,
318                 stderr=subprocess.STDOUT,
319                 shell=use_shell)
320         except subprocess.CalledProcessError as e:
321             if input_api.is_committing:
322                 return [output_api.PresubmitError('export_targets.py failed: %s' % e.output)]
323             return [
324                 output_api.PresubmitPromptWarning(
325                     'export_targets.py failed, this may just be due to your local checkout: %s' %
326                     e.output)
327             ]
328         return []
329     finally:
330         shutil.rmtree(outdir)
331
332
333 def _CheckTabsInSourceFiles(input_api, output_api):
334     """Forbids tab characters in source files due to a WebKit repo requirement."""
335
336     def implementation_and_headers_including_third_party(f):
337         # Check third_party files too, because WebKit's checks don't make exceptions.
338         return input_api.FilterSourceFile(
339             f,
340             files_to_check=(r'.+%s' % _IMPLEMENTATION_AND_HEADER_EXTENSIONS,),
341             files_to_skip=[f for f in input_api.DEFAULT_FILES_TO_SKIP if not "third_party" in f])
342
343     files_with_tabs = []
344     for f in input_api.AffectedSourceFiles(implementation_and_headers_including_third_party):
345         for (num, line) in f.ChangedContents():
346             if '\t' in line:
347                 files_with_tabs.append(f)
348                 break
349
350     if files_with_tabs:
351         return [
352             output_api.PresubmitError(
353                 'Tab characters in source files.',
354                 items=sorted(files_with_tabs),
355                 long_text=
356                 'Tab characters are forbidden in ANGLE source files because WebKit\'s Subversion\n'
357                 'repository does not allow tab characters in source files.\n'
358                 'Please remove tab characters from these files.')
359         ]
360     return []
361
362
363 # https://stackoverflow.com/a/196392
364 def is_ascii(s):
365     return all(ord(c) < 128 for c in s)
366
367
368 def _CheckNonAsciiInSourceFiles(input_api, output_api):
369     """Forbids non-ascii characters in source files."""
370
371     def implementation_and_headers(f):
372         return input_api.FilterSourceFile(
373             f, files_to_check=(r'.+%s' % _IMPLEMENTATION_AND_HEADER_EXTENSIONS,))
374
375     files_with_non_ascii = []
376     for f in input_api.AffectedSourceFiles(implementation_and_headers):
377         for (num, line) in f.ChangedContents():
378             if not is_ascii(line):
379                 files_with_non_ascii.append("%s: %s" % (f, line))
380                 break
381
382     if files_with_non_ascii:
383         return [
384             output_api.PresubmitError(
385                 'Non-ASCII characters in source files.',
386                 items=sorted(files_with_non_ascii),
387                 long_text='Non-ASCII characters are forbidden in ANGLE source files.\n'
388                 'Please remove non-ASCII characters from these files.')
389         ]
390     return []
391
392
393 def _CheckCommentBeforeTestInTestFiles(input_api, output_api):
394     """Require a comment before TEST_P() and other tests."""
395
396     def test_files(f):
397         return input_api.FilterSourceFile(
398             f, files_to_check=(r'^src/tests/.+\.cpp$', r'^src/.+_unittest\.cpp$'))
399
400     tests_with_no_comment = []
401     for f in input_api.AffectedSourceFiles(test_files):
402         diff = f.GenerateScmDiff()
403         last_line_was_comment = False
404         for line in diff.splitlines():
405             # Skip removed lines
406             if line.startswith('-'):
407                 continue
408
409             new_line_is_comment = line.startswith(' //') or line.startswith('+//')
410             new_line_is_test_declaration = (
411                 line.startswith('+TEST_P(') or line.startswith('+TEST(') or
412                 line.startswith('+TYPED_TEST('))
413
414             if new_line_is_test_declaration and not last_line_was_comment:
415                 tests_with_no_comment.append(line[1:])
416
417             last_line_was_comment = new_line_is_comment
418
419     if tests_with_no_comment:
420         return [
421             output_api.PresubmitError(
422                 'Tests without comment.',
423                 items=sorted(tests_with_no_comment),
424                 long_text='ANGLE requires a comment describing what a test does.')
425         ]
426     return []
427
428
429 def _CheckShaderVersionInShaderLangHeader(input_api, output_api):
430     """Requires an update to ANGLE_SH_VERSION when ShaderLang.h or ShaderVars.h change."""
431
432     def headers(f):
433         return input_api.FilterSourceFile(
434             f,
435             files_to_check=(r'^include/GLSLANG/ShaderLang.h$', r'^include/GLSLANG/ShaderVars.h$'))
436
437     headers_changed = input_api.AffectedSourceFiles(headers)
438     if len(headers_changed) == 0:
439         return []
440
441     # Skip this check for reverts and rolls.  Unlike
442     # _CheckCommitMessageFormatting, relands are still checked because the
443     # original change might have incremented the version correctly, but the
444     # rebase over a new version could accidentally remove that (because another
445     # change in the meantime identically incremented it).
446     git_output = input_api.change.DescriptionText()
447     multiple_commits = _SplitIntoMultipleCommits(git_output)
448     for commit in multiple_commits:
449         if commit.startswith('Revert') or commit.startswith('Roll'):
450             return []
451
452     diffs = '\n'.join(f.GenerateScmDiff() for f in headers_changed)
453     versions = dict(re.findall(r'^([-+])#define ANGLE_SH_VERSION\s+(\d+)', diffs, re.M))
454
455     if len(versions) != 2 or int(versions['+']) <= int(versions['-']):
456         return [
457             output_api.PresubmitError(
458                 'ANGLE_SH_VERSION should be incremented when ShaderLang.h or ShaderVars.h change.',
459             )
460         ]
461     return []
462
463
464 def _CheckGClientExists(input_api, output_api, search_limit=None):
465     presubmit_path = pathlib.Path(input_api.PresubmitLocalPath())
466
467     for current_path in itertools.chain([presubmit_path], presubmit_path.parents):
468         gclient_path = current_path.joinpath('.gclient')
469         if gclient_path.exists() and gclient_path.is_file():
470             return []
471         # search_limit parameter is used in unit tests to prevent searching all the way to root
472         # directory for reproducibility.
473         elif search_limit != None and current_path == search_limit:
474             break
475
476     return [
477         output_api.PresubmitError(
478             'Missing .gclient file.',
479             long_text=textwrap.fill(
480                 width=100,
481                 text='The top level directory of the repository must contain a .gclient file.'
482                 ' You can follow the steps outlined in the link below to get set up for ANGLE'
483                 ' development:') +
484             '\n\nhttps://chromium.googlesource.com/angle/angle/+/refs/heads/main/doc/DevSetup.md')
485     ]
486
487
488 def CheckChangeOnUpload(input_api, output_api):
489     results = []
490     results.extend(_CheckTabsInSourceFiles(input_api, output_api))
491     results.extend(_CheckNonAsciiInSourceFiles(input_api, output_api))
492     results.extend(_CheckCommentBeforeTestInTestFiles(input_api, output_api))
493     results.extend(_CheckShaderVersionInShaderLangHeader(input_api, output_api))
494     results.extend(_CheckCodeGeneration(input_api, output_api))
495     results.extend(_CheckChangeHasBugField(input_api, output_api))
496     results.extend(input_api.canned_checks.CheckChangeHasDescription(input_api, output_api))
497     results.extend(_CheckNewHeaderWithoutGnChange(input_api, output_api))
498     results.extend(_CheckExportValidity(input_api, output_api))
499     results.extend(
500         input_api.canned_checks.CheckPatchFormatted(
501             input_api, output_api, result_factory=output_api.PresubmitError))
502     results.extend(_CheckCommitMessageFormatting(input_api, output_api))
503     results.extend(_CheckGClientExists(input_api, output_api))
504
505     return results
506
507
508 def CheckChangeOnCommit(input_api, output_api):
509     return CheckChangeOnUpload(input_api, output_api)