
Reland notes #2: The reason the previous reland was reverted was dealt in a precursor CL (see crbug.com/398274433) and is no longer part of this reland. Bug: 397907153 Change-Id: I2ee1871ad10f3d607634085a31eb5c37e61e8dd2 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6293728 Reviewed-by: Rohit Rao <rohitrao@chromium.org> Auto-Submit: Demetrios Papadopoulos <dpapad@chromium.org> Commit-Queue: Demetrios Papadopoulos <dpapad@chromium.org> Cr-Commit-Position: refs/heads/main@{#1424698}
452 lines
16 KiB
Python
452 lines
16 KiB
Python
# Copyright 2017 The Chromium Authors
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
"""Presubmit script for ios.
|
|
|
|
See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
|
|
for more details about the presubmit API built into depot_tools.
|
|
"""
|
|
|
|
import os
|
|
import xml.etree.ElementTree as ElementTree
|
|
|
|
NULLABILITY_PATTERN = r'(nonnull|nullable|_Nullable|_Nonnull)'
|
|
TODO_PATTERN = r'TO[D]O\(([^\)]*)\)'
|
|
BUG_PATTERN = r'^(crbug\.com|b)/\d+$'
|
|
DEPRECATED_BUG_PATTERN = r'^b/\d+$'
|
|
INCLUDE_PATTERN = r'^#include'
|
|
PIPE_IN_COMMENT_PATTERN = r'//.*[^|]\|(?!\|)'
|
|
IOS_PACKAGE_PATTERN = r'^ios'
|
|
BOXED_BOOL_PATTERN = r'@\((YES|NO)\)'
|
|
USER_DEFAULTS_PATTERN = r'\[NSUserDefaults standardUserDefaults]'
|
|
|
|
# Color management constants
|
|
COLOR_SHARED_DIR = 'ios/chrome/common/ui/colors/'
|
|
COLOR_FILE_PATTERN = '.colorset/Contents.json'
|
|
|
|
|
|
def FormatMessageWithFiles(message, errors):
|
|
"""Helper to format warning/error messages with affected files."""
|
|
if not errors:
|
|
return message
|
|
return '\n'.join([message + '\n\nAffected file(s):'] + errors) + '\n'
|
|
|
|
def IsSubListOf(needle, hay):
|
|
"""Returns whether there is a slice of |hay| equal to |needle|."""
|
|
for i, line in enumerate(hay):
|
|
if line == needle[0]:
|
|
if needle == hay[i:i + len(needle)]:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _CheckNullabilityAnnotations(input_api, output_api):
|
|
""" Checks whether there are nullability annotations in ios code.
|
|
|
|
They are accepted in ios/web_view/public since it tries to mimic
|
|
the platform library but not anywhere else.
|
|
"""
|
|
nullability_regex = input_api.re.compile(NULLABILITY_PATTERN)
|
|
|
|
errors = []
|
|
for f in input_api.AffectedFiles():
|
|
if f.LocalPath().startswith('ios/web_view/public/'):
|
|
# ios/web_view/public tries to mimic an existing API that
|
|
# might have nullability in it and that is acceptable.
|
|
continue
|
|
for line_num, line in f.ChangedContents():
|
|
if nullability_regex.search(line):
|
|
errors.append('%s:%s' % (f.LocalPath(), line_num))
|
|
if not errors:
|
|
return []
|
|
|
|
plural_suffix = '' if len(errors) == 1 else 's'
|
|
warning_message = ('Found Nullability annotation%(plural)s. '
|
|
'Prefer DCHECKs in ios code to check for nullness:' % {
|
|
'plural': plural_suffix
|
|
})
|
|
|
|
return [output_api.PresubmitPromptWarning(warning_message, items=errors)]
|
|
|
|
|
|
def _CheckBugInToDo(input_api, output_api):
|
|
""" Checks whether TODOs in ios code are identified by a bug number."""
|
|
errors = []
|
|
warnings = []
|
|
for f in input_api.AffectedFiles():
|
|
for line_num, line in f.ChangedContents():
|
|
if _HasToDoWithNoBug(input_api, line):
|
|
errors.append('%s:%s' % (f.LocalPath(), line_num))
|
|
if _HasToDoWithDeprecatedBug(input_api, line):
|
|
warnings.append('%s:%s' % (f.LocalPath(), line_num))
|
|
if not errors and not warnings:
|
|
return []
|
|
|
|
output = []
|
|
if errors:
|
|
singular_article = 'a ' if len(errors) == 1 else ''
|
|
plural_suffix = '' if len(errors) == 1 else 's'
|
|
error_message = '\n'.join([
|
|
'Found TO'
|
|
'DO%(plural)s without %(a)sbug number%(plural)s (expected format '
|
|
'is \"TO'
|
|
'DO(crbug.com/######)\"):' % {
|
|
'plural': plural_suffix,
|
|
'a' : singular_article
|
|
}
|
|
] + errors) + '\n'
|
|
output.append(output_api.PresubmitError(error_message))
|
|
|
|
if warnings:
|
|
singular_article = 'a ' if len(warnings) == 1 else ''
|
|
plural_suffix = '' if len(warnings) == 1 else 's'
|
|
warning_message = '\n'.join([
|
|
'Found TO'
|
|
'DO%(plural)s with %(a)sdeprecated bug link%(plural)s (found '
|
|
'"b/#####\", expected format is \"crbug.com/######"):' % {
|
|
'plural': plural_suffix,
|
|
'a' : singular_article
|
|
}
|
|
] + warnings) + '\n'
|
|
output.append(output_api.PresubmitPromptWarning(warning_message))
|
|
|
|
return output
|
|
|
|
|
|
def _CheckHasNoIncludeDirectives(input_api, output_api):
|
|
""" Checks that #include preprocessor directives are not present."""
|
|
errors = []
|
|
for f in input_api.AffectedFiles():
|
|
if not _IsInIosPackage(input_api, f.LocalPath()):
|
|
continue
|
|
_, ext = os.path.splitext(f.LocalPath())
|
|
if ext != '.mm':
|
|
continue
|
|
for line_num, line in f.ChangedContents():
|
|
if _HasIncludeDirective(input_api, line):
|
|
errors.append('%s:%s' % (f.LocalPath(), line_num))
|
|
if not errors:
|
|
return []
|
|
|
|
singular_plural = 'it' if len(errors) == 1 else 'them'
|
|
plural_suffix = '' if len(errors) == 1 else 's'
|
|
error_message = '\n'.join([
|
|
'Found usage of `#include` preprocessor directive%(plural)s! Please, '
|
|
'replace %(singular_plural)s with `#import` preprocessor '
|
|
'directive%(plural)s instead. '
|
|
'Consider replacing all existing `#include` with `#import` (if any) in '
|
|
'this file for the code clean up. See '
|
|
'https://chromium.googlesource.com/chromium/src.git/+/refs/heads/main'
|
|
'/styleguide/objective-c/objective-c.md'
|
|
'#import-and-include-in-the-directory for more details. '
|
|
'\n\nAffected file%(plural)s:' % {
|
|
'plural': plural_suffix,
|
|
'singular_plural': singular_plural
|
|
}
|
|
] + errors) + '\n'
|
|
|
|
return [output_api.PresubmitError(error_message)]
|
|
|
|
|
|
def _CheckHasNoPipeInComment(input_api, output_api):
|
|
""" Checks that comments don't contain pipes."""
|
|
pipe_regex = input_api.re.compile(PIPE_IN_COMMENT_PATTERN)
|
|
|
|
errors = []
|
|
for f in input_api.AffectedFiles():
|
|
if not _IsInIosPackage(input_api, f.LocalPath()):
|
|
continue
|
|
for line_num, line in f.ChangedContents():
|
|
if pipe_regex.search(line):
|
|
errors.append('%s:%s' % (f.LocalPath(), line_num))
|
|
if not errors:
|
|
return []
|
|
warning_message = '\n'.join([
|
|
'Please use backticks "`" instead of pipes "|" if you need to quote'
|
|
' variable names and symbols in comments.\n'
|
|
'Found potential uses of pipes in:'
|
|
] + errors) + '\n'
|
|
|
|
return [output_api.PresubmitPromptWarning(warning_message)]
|
|
|
|
def _CheckCanImproveTestUsingExpectNSEQ(input_api, output_api):
|
|
""" Checks that test files use EXPECT_NSEQ when possible."""
|
|
errors = []
|
|
# Substrings that should not be used together with EXPECT_TRUE or
|
|
# EXPECT_FALSE in tests.
|
|
wrong_patterns = ["isEqualToString:", "isEqualToData:", "isEqualToArray:"]
|
|
for f in input_api.AffectedFiles():
|
|
if not '_unittest.' in f.LocalPath():
|
|
continue
|
|
for line_num, line in f.ChangedContents():
|
|
if line.startswith(("EXPECT_TRUE", "EXPECT_FALSE")):
|
|
# Condition is in one line.
|
|
if any(x in line for x in wrong_patterns):
|
|
errors.append('%s:%s' % (f.LocalPath(), line_num))
|
|
# Condition is split on multiple lines.
|
|
elif not line.endswith(";"):
|
|
# Check this is not the last line.
|
|
if line_num < len(f.NewContents()):
|
|
next_line = f.NewContents()[line_num]
|
|
if any(x in next_line for x in wrong_patterns):
|
|
errors.append('%s:%s' % (f.LocalPath(), line_num))
|
|
|
|
if not errors:
|
|
return []
|
|
|
|
plural_suffix = '' if len(errors) == 1 else 's'
|
|
warning_message = '\n'.join([
|
|
'Found possible improvement in unittest. Prefer using'
|
|
' EXPECT_NSEQ() or EXPECT_NSNE() when possible.'
|
|
'\n\nAffected file%(plural)s:' % {
|
|
'plural': plural_suffix,
|
|
}
|
|
] + errors) + '\n'
|
|
|
|
return [output_api.PresubmitPromptWarning(warning_message)]
|
|
|
|
def _IsInIosPackage(input_api, path):
|
|
""" Returns True if path is within ios package"""
|
|
ios_package_regex = input_api.re.compile(IOS_PACKAGE_PATTERN)
|
|
|
|
return ios_package_regex.search(path)
|
|
|
|
|
|
def _HasIncludeDirective(input_api, line):
|
|
""" Returns True if #include is found in the line"""
|
|
include_regex = input_api.re.compile(INCLUDE_PATTERN)
|
|
|
|
return include_regex.search(line)
|
|
|
|
|
|
def _HasToDoWithNoBug(input_api, line):
|
|
""" Returns True if TODO is not identified by a bug number."""
|
|
todo_regex = input_api.re.compile(TODO_PATTERN)
|
|
bug_regex = input_api.re.compile(BUG_PATTERN)
|
|
|
|
todo_match = todo_regex.search(line)
|
|
if not todo_match:
|
|
return False
|
|
|
|
return not bug_regex.match(todo_match.group(1))
|
|
|
|
def _HasToDoWithDeprecatedBug(input_api, line):
|
|
""" Returns True if TODO is identified by a deprecated bug number format."""
|
|
todo_regex = input_api.re.compile(TODO_PATTERN)
|
|
deprecated_bug_regex = input_api.re.compile(DEPRECATED_BUG_PATTERN)
|
|
|
|
todo_match = todo_regex.search(line)
|
|
if not todo_match:
|
|
return False
|
|
return deprecated_bug_regex.match(todo_match.group(1))
|
|
|
|
def _CheckHasNoBoxedBOOL(input_api, output_api):
|
|
""" Checks that there are no @(YES) or @(NO)."""
|
|
boxed_BOOL_regex = input_api.re.compile(BOXED_BOOL_PATTERN)
|
|
|
|
errors = []
|
|
for f in input_api.AffectedFiles():
|
|
for line_num, line in f.ChangedContents():
|
|
if boxed_BOOL_regex.search(line):
|
|
errors.append('%s:%s' % (f.LocalPath(), line_num))
|
|
if not errors:
|
|
return []
|
|
|
|
plural_suffix = '' if len(errors) == 1 else 's'
|
|
warning_message = ('Found boxed BOOL%(plural)s. '
|
|
'Prefer @YES or @NO in ios code:' % {
|
|
'plural': plural_suffix
|
|
})
|
|
|
|
return [output_api.PresubmitPromptWarning(warning_message, items=errors)]
|
|
|
|
def _CheckNoTearDownEGTest(input_api, output_api):
|
|
""" Checks that `- (void)tearDown {` is not present in an egtest.mm"""
|
|
errors = []
|
|
for f in input_api.AffectedFiles():
|
|
if not '_egtest.' in f.LocalPath():
|
|
continue
|
|
for line_num, line in f.ChangedContents():
|
|
if line.startswith("- (void)tearDown {"):
|
|
errors.append('%s:%s' % (f.LocalPath(), line_num))
|
|
|
|
if not errors:
|
|
return []
|
|
warning_message = '\n'.join([
|
|
'To support hermetic EarlGrey test cases, tearDown has been renamed '
|
|
'to tearDownHelper, and will soon be removed. If tearDown is really '
|
|
'necessary for this test, please use addTeardownBlock'
|
|
] + errors) + '\n'
|
|
|
|
return [output_api.PresubmitError(warning_message)]
|
|
|
|
|
|
def _IsAlphabeticallySortedXML(file):
|
|
"""Check that the `file` is alphabetically sorted"""
|
|
parser = ElementTree.XMLParser(target=ElementTree.TreeBuilder(
|
|
insert_comments=True))
|
|
with open(file, 'r', encoding='utf8') as xml_file:
|
|
tree = ElementTree.parse(xml_file, parser)
|
|
root = tree.getroot()
|
|
|
|
original_tree_string = ElementTree.tostring(root, encoding='utf8')
|
|
|
|
messages_element = tree.findall('.//messages')[0]
|
|
messages = messages_element.findall('message')
|
|
messages.sort(key=lambda message: message.attrib["name"])
|
|
for message in messages:
|
|
messages_element.remove(message)
|
|
for message in messages:
|
|
messages_element.append(message)
|
|
ordered_tree_string = ElementTree.tostring(root, encoding='utf8')
|
|
return ordered_tree_string == original_tree_string
|
|
|
|
|
|
def _CheckOrderedStringFile(input_api, output_api):
|
|
""" Checks that the string files are alphabetically ordered"""
|
|
errors = []
|
|
for f in input_api.AffectedFiles():
|
|
if not f.LocalPath().endswith("_strings.grd"):
|
|
continue
|
|
if not _IsAlphabeticallySortedXML(f.AbsoluteLocalPath()):
|
|
errors.append(' python3 ios/tools/order_string_file.py ' +
|
|
f.LocalPath())
|
|
|
|
if not errors:
|
|
return []
|
|
warning_message = '\n'.join(
|
|
['Files not alphabetically sorted, try running:'] + errors) + '\n'
|
|
|
|
return [output_api.PresubmitPromptWarning(warning_message)]
|
|
|
|
|
|
def _CheckNotUsingNSUserDefaults(input_api, output_api):
|
|
""" Checks the added code to limit new usage of NSUserDefaults """
|
|
user_defaults_regex = input_api.re.compile(USER_DEFAULTS_PATTERN)
|
|
|
|
errors = []
|
|
for f in input_api.AffectedFiles():
|
|
if (not f.LocalPath().endswith('.mm')):
|
|
continue
|
|
for line_num, line in f.ChangedContents():
|
|
if user_defaults_regex.search(line):
|
|
errors.append('%s:%s' % (f.LocalPath(), line_num))
|
|
|
|
if not errors:
|
|
return []
|
|
warning_message = '\n'.join([
|
|
'A new use of NSUserDefaults was added. If this is a newly added key '
|
|
'consider storing it to PrefService instead.'
|
|
] + errors) + '\n'
|
|
|
|
return [output_api.PresubmitPromptWarning(warning_message)]
|
|
|
|
|
|
def _CheckNewColorIntroduction(input_api, output_api):
|
|
"""Checks for new or modified colorset files.
|
|
|
|
Ensures colors are properly added to the shared directory.
|
|
"""
|
|
results = []
|
|
|
|
affected_files = [
|
|
f for f in input_api.AffectedFiles()
|
|
if f.LocalPath().endswith(COLOR_FILE_PATTERN)
|
|
]
|
|
|
|
warnings = {
|
|
'shared_added': [],
|
|
'shared_modified': [],
|
|
'other_modified': []
|
|
}
|
|
errors = []
|
|
|
|
for affected_file in affected_files:
|
|
action = affected_file.Action()
|
|
local_path = affected_file.LocalPath()
|
|
file_path_error = '%s' % (affected_file.LocalPath())
|
|
|
|
if COLOR_SHARED_DIR in local_path:
|
|
if action == 'A':
|
|
warnings['shared_added'].append(file_path_error)
|
|
elif action == 'M':
|
|
warnings['shared_modified'].append(file_path_error)
|
|
else:
|
|
if action == 'A':
|
|
errors.append(file_path_error)
|
|
elif action == 'M':
|
|
warnings['other_modified'].append(file_path_error)
|
|
|
|
output = []
|
|
|
|
if errors:
|
|
error_message = ('New color(s) must be added to the %s directory.' %
|
|
COLOR_SHARED_DIR)
|
|
output.append(
|
|
output_api.PresubmitError(
|
|
FormatMessageWithFiles(error_message, errors)))
|
|
|
|
warning_message = ('Please ensure the color does not already exist in the '
|
|
'shared %s directory.' % COLOR_SHARED_DIR)
|
|
|
|
if warnings['shared_added']:
|
|
shared_added_message = ('New color(s) added in %s. %s' %
|
|
(COLOR_SHARED_DIR, warning_message))
|
|
output.append(
|
|
output_api.PresubmitPromptWarning(
|
|
FormatMessageWithFiles(shared_added_message,
|
|
warnings['shared_added'])))
|
|
|
|
if warnings['shared_modified']:
|
|
shared_modified_message = ('Color(s) modified in %s. %s' %
|
|
(COLOR_SHARED_DIR, warning_message))
|
|
output.append(
|
|
output_api.PresubmitPromptWarning(
|
|
FormatMessageWithFiles(shared_modified_message,
|
|
warnings['shared_modified'])))
|
|
|
|
if warnings['other_modified']:
|
|
modified_message = ('Color(s) modified. %s' % warning_message)
|
|
output.append(
|
|
output_api.PresubmitPromptWarning(
|
|
FormatMessageWithFiles(modified_message,
|
|
warnings['other_modified'])))
|
|
|
|
return output
|
|
|
|
def _CheckStyleESLint(input_api, output_api):
|
|
results = []
|
|
|
|
try:
|
|
import sys
|
|
old_sys_path = sys.path[:]
|
|
cwd = input_api.PresubmitLocalPath()
|
|
sys.path += [input_api.os_path.join(cwd, '..', 'tools')]
|
|
from web_dev_style import presubmit_support
|
|
results += presubmit_support.CheckStyleESLint(input_api, output_api)
|
|
finally:
|
|
sys.path = old_sys_path
|
|
|
|
return results
|
|
|
|
def CheckChange(input_api, output_api):
|
|
results = []
|
|
results.extend(_CheckBugInToDo(input_api, output_api))
|
|
results.extend(_CheckNullabilityAnnotations(input_api, output_api))
|
|
results.extend(_CheckHasNoIncludeDirectives(input_api, output_api))
|
|
results.extend(_CheckHasNoPipeInComment(input_api, output_api))
|
|
results.extend(_CheckHasNoBoxedBOOL(input_api, output_api))
|
|
results.extend(_CheckNoTearDownEGTest(input_api, output_api))
|
|
results.extend(_CheckCanImproveTestUsingExpectNSEQ(input_api, output_api))
|
|
results.extend(_CheckOrderedStringFile(input_api, output_api))
|
|
results.extend(_CheckNotUsingNSUserDefaults(input_api, output_api))
|
|
results.extend(_CheckNewColorIntroduction(input_api, output_api))
|
|
results.extend(_CheckStyleESLint(input_api, output_api))
|
|
return results
|
|
|
|
def CheckChangeOnUpload(input_api, output_api):
|
|
return CheckChange(input_api, output_api)
|
|
|
|
def CheckChangeOnCommit(input_api, output_api):
|
|
return CheckChange(input_api, output_api)
|