| # 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 subprocess |
| 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]' |
| UMBRELLA_HEADER_PATTERN = r'#import\s+<([\w]+)\/(?!\1\.h)[^>]+>' |
| UNITTEST_FILE_PATTERN = r'_unittests?\.(mm|cc)$' |
| SYSTEM_COLORS_PATTERN = ( |
| r'\[UIColor (?!white|black|clear)[a-z][a-zA-Z]*Color\]|' |
| r'UIColor\.(?!white|black|clear)[a-z][a-zA-Z]*Color') |
| |
| # 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/') or |
| f.LocalPath().startswith('ios/third_party/')): |
| # 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(include_deletes=False): |
| 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 _CheckOrderedFlagsFile(input_api, output_api): |
| """ Checks that the flag description files are alphabetically ordered""" |
| # The order_flags.py script calls `gclient root` which is not available |
| # on Windows CI bots, causing all CLs that touch |
| # ios_chrome_flag_descriptions.{h,cc} to fail presubmit on Windows. |
| if input_api.platform == 'win32': |
| return [] |
| h_file = None |
| for f in input_api.AffectedFiles(include_deletes=False): |
| if f.LocalPath().endswith('ios_chrome_flag_descriptions.h'): |
| h_file = f.LocalPath() |
| |
| if h_file: |
| try: |
| command = [ |
| input_api.python3_executable, 'tools/order_flags.py', '--check' |
| ] |
| if h_file: |
| command.extend(['--h-file', h_file]) |
| subprocess.check_output(command, stderr=subprocess.STDOUT) |
| except subprocess.CalledProcessError as e: |
| message = 'Flag description files not alphabetically sorted.\n' |
| message += e.output.decode('utf-8') |
| message += '\nPlease run: python3 ios/tools/order_flags.py' |
| return [output_api.PresubmitError(message)] |
| |
| return [] |
| |
| |
| 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 _CheckUIGraphicsBeginImageContextWithOptions(input_api, output_api): |
| """ Checks that UIGraphicsBeginImageContext and |
| UIGraphicsBeginImageContextWithOptions are not used""" |
| deprecated_regex = input_api.re.compile( |
| r'UIGraphicsBeginImageContext(WithOptions)?\(') |
| |
| errors = [] |
| for f in input_api.AffectedFiles(): |
| if (not f.LocalPath().endswith('.mm')): |
| continue |
| for line_num, line in f.ChangedContents(): |
| if deprecated_regex.search(line): |
| errors.append('%s:%s' % (f.LocalPath(), line_num)) |
| |
| if not errors: |
| return [] |
| error_message = '\n'.join([ |
| 'UIGraphicsBeginImageContext and ' |
| 'UIGraphicsBeginImageContextWithOptions are deprecated, use ' |
| 'UIGraphicsImageRenderer instead.' |
| ] + errors) + '\n' |
| |
| return [output_api.PresubmitError(error_message)] |
| |
| |
| def _CheckOmniboxTextInEgtest(input_api, output_api): |
| """Checks use of OmniboxText or chrome_test_util::OmniboxText in egtests. |
| """ |
| pattern = input_api.re.compile(r'(OmniboxText)') |
| |
| errors = [] |
| for f in input_api.AffectedFiles(): |
| if not f.LocalPath().endswith('_egtest.mm'): |
| continue |
| for line_num, line in f.ChangedContents(): |
| if pattern.search(line): |
| errors.append('%s:%s' % (f.LocalPath(), line_num)) |
| |
| if not errors: |
| return [] |
| warning_message = '\n'.join([ |
| 'Please use [ChromeEarlGrey waitForWebStateVisibleURL:] to check for ' |
| 'URL load in the browser' |
| ] + errors) + '\n' |
| |
| return [output_api.PresubmitPromptWarning(warning_message)] |
| |
| def _CheckUmbrellaHeaderUsage(input_api, output_api): |
| """Checks for individual system header imports instead of umbrella headers. |
| """ |
| umbrella_regex = input_api.re.compile(UMBRELLA_HEADER_PATTERN) |
| |
| errors = [] |
| file_filter = lambda f: f.LocalPath().endswith(('.mm', '.h')) |
| |
| for f in input_api.AffectedSourceFiles(file_filter): |
| for line_num, line in f.ChangedContents(): |
| match = umbrella_regex.search(line) |
| if match: |
| framework_name = match.group(1) |
| errors.append('%s:%s (Found: %s. Use <%s/%s.h> instead)' % |
| (f.LocalPath(), line_num, line.strip(), |
| framework_name, framework_name)) |
| if not errors: |
| return [] |
| |
| warning_message = ( |
| 'Always use Umbrella Headers for system frameworks. Importing ' |
| 'individual headers increases compilation time and breaks module ' |
| 'optimization.' |
| ) |
| |
| return [ |
| output_api.PresubmitPromptWarning(warning_message, items=errors) |
| ] |
| |
| def _CheckNoFlakyUnitTest(input_api, output_api): |
| """ Checks that there are no tests with FLAKY_ prefix.""" |
| flaky_specifier_pattern = input_api.re.compile(r'\bFLAKY_') |
| |
| unittest_regex = input_api.re.compile(UNITTEST_FILE_PATTERN) |
| errors = [] |
| for f in input_api.AffectedFiles(): |
| if not unittest_regex.search(f.LocalPath()): |
| continue |
| for line_num, line in f.ChangedContents(): |
| if line.lstrip().startswith('//'): |
| continue |
| if flaky_specifier_pattern.search(line): |
| errors.append('%s:%s' % (f.LocalPath(), line_num)) |
| |
| if not errors: |
| return [] |
| error_message = '\n'.join([ |
| 'Unit tests must not be marked as FLAKY_. If a test is flaky, use ' |
| 'DISABLED_ instead.' |
| ] + errors) + '\n' |
| |
| return [output_api.PresubmitError(error_message)] |
| |
| def _CheckUsageOfSystemColors(input_api, output_api): |
| """ Checks whether there are forbidden system color used in ios code. |
| |
| Only white, black and clear colors are accepted. |
| """ |
| system_colors_regex = input_api.re.compile(SYSTEM_COLORS_PATTERN) |
| |
| errors = [] |
| file_filter = lambda f: f.LocalPath().endswith(('.mm', '.h')) |
| for f in input_api.AffectedSourceFiles(file_filter): |
| for line_num, line in f.ChangedContents(): |
| if system_colors_regex.search(line): |
| errors.append('%s:%s' % (f.LocalPath(), line_num)) |
| if not errors: |
| return [] |
| |
| warning_message = 'Found forbidden usage of system colors. ' |
| 'Only white, black and clear colors are accepted.' |
| 'Prefer semantic colors in ios code instead of the system ones:' |
| |
| return [output_api.PresubmitPromptWarning(warning_message, items=errors)] |
| |
| |
| def _CheckLargeImagesets(input_api, output_api): |
| """Checks that no large (> 100pt) PDF or SVG files are added to |
| .imageset directories. |
| """ |
| import re |
| |
| def get_svg_dimensions(file_contents): |
| try: |
| import xml.etree.ElementTree as ET |
| if isinstance(file_contents, bytes): |
| text = file_contents.decode('utf-8', errors='ignore') |
| else: |
| text = file_contents |
| |
| root = ET.fromstring(text) |
| w_str = root.attrib.get('width', '') |
| h_str = root.attrib.get('height', '') |
| viewbox_str = (root.attrib.get('viewBox', '') or |
| root.attrib.get('viewbox', '')) |
| |
| def parse_val(v): |
| if not v: |
| return 0.0 |
| m = re.match(r'^([\d.]+)', str(v).strip()) |
| return float(m.group(1)) if m else 0.0 |
| |
| w, h = parse_val(w_str), parse_val(h_str) |
| if (w == 0.0 or h == 0.0) and viewbox_str: |
| parts = re.split(r'[\s,]+', viewbox_str.strip()) |
| if len(parts) >= 4: |
| w = parse_val(parts[2]) |
| h = parse_val(parts[3]) |
| return w, h |
| except Exception: |
| return 0.0, 0.0 |
| |
| def get_pdf_dimensions(file_contents): |
| try: |
| if isinstance(file_contents, str): |
| file_bytes = file_contents.encode('latin-1', errors='ignore') |
| else: |
| file_bytes = file_contents |
| |
| # The PDF spec specifies that MediaBox can have whitespace or |
| # newlines. |
| pattern = re.compile( |
| rb'/MediaBox\s*\[\s*(-?[\d.]+)\s+(-?[\d.]+)\s+' |
| rb'(-?[\d.]+)\s+(-?[\d.]+)\s*\]') |
| match = pattern.search(file_bytes) |
| if match: |
| llx = float(match.group(1)) |
| lly = float(match.group(2)) |
| urx = float(match.group(3)) |
| ury = float(match.group(4)) |
| return abs(urx - llx), abs(ury - lly) |
| except Exception: |
| pass |
| return 0.0, 0.0 |
| |
| errors = [] |
| for f in input_api.AffectedFiles(include_deletes=False): |
| if f.Action() != 'A': |
| continue |
| local_path = f.LocalPath() |
| lower_path = local_path.lower() |
| if '.imageset/' not in lower_path: |
| continue |
| |
| if lower_path.endswith('.svg'): |
| is_svg = True |
| elif lower_path.endswith('.pdf'): |
| is_svg = False |
| else: |
| continue |
| |
| try: |
| with open(f.AbsoluteLocalPath(), 'rb') as fp: |
| file_contents = fp.read() |
| except (IOError, OSError): |
| file_contents = input_api.ReadFile( |
| f.AbsoluteLocalPath(), mode='rb') |
| |
| if is_svg: |
| w, h = get_svg_dimensions(file_contents) |
| else: |
| w, h = get_pdf_dimensions(file_contents) |
| |
| if w > 100.0 or h > 100.0: |
| errors.append('%s (%.1fx%.1f pt)' % (local_path, w, h)) |
| |
| if not errors: |
| return [] |
| |
| warning_message = ( |
| 'Large vector assets (> 100pt) found in .imageset directories.\n' |
| 'Apple actool rasterizes these at compile time into uncompressed\n' |
| '32-bit ARGB bitmaps, which severely bloats the Assets.car binary\n' |
| 'size. Please use @2x and @3x PNGs instead.\n' |
| 'Files:\n' |
| ) |
| |
| return [output_api.PresubmitError(warning_message, items=errors)] |
| |
| |
| 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(_CheckOrderedFlagsFile(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)) |
| results.extend( |
| _CheckUIGraphicsBeginImageContextWithOptions(input_api, output_api)) |
| results.extend(_CheckOmniboxTextInEgtest(input_api, output_api)) |
| results.extend(_CheckUmbrellaHeaderUsage(input_api, output_api)) |
| results.extend(_CheckNoFlakyUnitTest(input_api, output_api)) |
| results.extend(_CheckUsageOfSystemColors(input_api, output_api)) |
| results.extend(_CheckLargeImagesets(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) |