| #!/usr/bin/env python | |
| # | |
| # Copyright 2016-present the Material Components for iOS authors. All Rights | |
| # Reserved. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| import os | |
| import re | |
| import sys | |
| from collections import namedtuple | |
| # Prefix of all MDC umbrella headers. | |
| UMBRELLA_HEADER_PREFIX = 'Material' | |
| # Import prefixes that should not be checked. | |
| IMPORT_FILTER_PREFIXES = ( | |
| # Objective-C imports | |
| 'Availability.h', | |
| 'CoreFoundation/', | |
| 'CoreGraphics/', | |
| 'Foundation/', | |
| 'QuartzCore/', | |
| 'UIKit/', | |
| 'objc/', | |
| # Third-party imports | |
| 'MDF', | |
| 'Motion', | |
| ) | |
| # Import suffixes that should not be checked. | |
| IMPORT_FILTER_SUFFIXES = ( | |
| # Color themers do not have umbrella headers. | |
| 'ColorThemer.h', | |
| ) | |
| # Regex to match import of two kinds: | |
| # 1) #import "Foo.h", group #1 | |
| # 2) #import <Foo.h>, group #2 | |
| IMPORT_RE = re.compile(r'#import (\"(.*\.h)\"|\<(.*\.h)\>)') | |
| def umbrella_header(component): | |
| """Returns an umbrella header for the component.""" | |
| return '{}{}.h'.format(UMBRELLA_HEADER_PREFIX, component) | |
| def framework_umbrella_header(component): | |
| """Returns a framework umbrella header for the component.""" | |
| return 'MaterialComponents/{}'.format(umbrella_header(component)) | |
| # Context in which the check is executed. | |
| # @checked_file - the name of the file that is checked. | |
| # @module_files - list of files in currently analyzed sub-component. | |
| CheckContext = namedtuple('CheckContext', ['checked_file', 'module_files']) | |
| class ImportChecks(object): | |
| """A class containing factory methods for various checks and filters.""" | |
| @staticmethod | |
| def import_filter(): | |
| """Returns a function to filter Objective-C and third-party framework imports.""" | |
| def check(import_str, _): | |
| filter = import_str.startswith(IMPORT_FILTER_PREFIXES) or \ | |
| import_str.endswith(IMPORT_FILTER_SUFFIXES) | |
| return filter, None | |
| return check | |
| @staticmethod | |
| def module_import_filter(): | |
| """Returns a function to filter same module file imports.""" | |
| def check(import_str, check_context): | |
| return any(module_file.endswith(import_str) for module_file in check_context.module_files), None | |
| return check | |
| @staticmethod | |
| def non_umbrella_header_check(): | |
| """Returns a function to check that an umbrella header is imported.""" | |
| def check(import_str, check_context): | |
| if not import_str.startswith(UMBRELLA_HEADER_PREFIX): | |
| error_msg = '{} imports a non-umbrella header: {}'.format(check_context.checked_file, import_str) | |
| return True, error_msg | |
| return False, None | |
| return check | |
| @staticmethod | |
| def own_umbrella_header_check(umbrella_header): | |
| """Returns a function to check that checked component umbrella header is not used.""" | |
| def check(import_str, check_context): | |
| if import_str == umbrella_header: | |
| return True, '{} imports an own umbrella header: {}'.format(check_context.checked_file, import_str) | |
| return False, None | |
| return check | |
| class ImportChecker(object): | |
| """A class to run checks over the import strings.""" | |
| def __init__(self, check_functions): | |
| self.check_functions = check_functions | |
| def check_import(self, import_str, check_context): | |
| """Runs checks over the import string and returns an error message in check failed.""" | |
| for check in self.check_functions: | |
| stop, error_msg = check(import_str, check_context) | |
| if error_msg: | |
| return error_msg | |
| if stop: | |
| break | |
| return None | |
| @staticmethod | |
| def src_checker(component): | |
| """Returns an 'src' code checker.""" | |
| return ImportChecker([ | |
| ImportChecks.import_filter(), | |
| ImportChecks.own_umbrella_header_check(umbrella_header(component)), | |
| ImportChecks.own_umbrella_header_check(framework_umbrella_header(component)), | |
| ImportChecks.module_import_filter(), | |
| ImportChecks.non_umbrella_header_check(), | |
| ]) | |
| @staticmethod | |
| def general_checker(): | |
| """Returns a general purpose checker.""" | |
| return ImportChecker([ | |
| ImportChecks.import_filter(), | |
| ImportChecks.module_import_filter(), | |
| ImportChecks.non_umbrella_header_check(), | |
| ]) | |
| def imports_in_file(file_path): | |
| """Returns a list of import strings.""" | |
| imports = [] | |
| with open(file_path) as f: | |
| for line in f.readlines(): | |
| match = IMPORT_RE.match(line) | |
| if not match: | |
| # No imports found in file. | |
| continue | |
| groups = match.groups() | |
| if groups[1]: | |
| imports.append(groups[1]) | |
| elif groups[2]: | |
| imports.append(groups[2]) | |
| return imports | |
| def list_objc_files(path, skip_dirs=()): | |
| """Returns a list of Objective-C files in the path.""" | |
| files_relative_paths = [] | |
| for dirpath, dirnames, files in os.walk(path): | |
| for f in files: | |
| if os.path.basename(dirpath) in skip_dirs: | |
| continue | |
| if f.endswith(('.h', '.m')): | |
| files_relative_paths.append(os.path.relpath(os.path.join(dirpath, f), path)) | |
| return files_relative_paths | |
| def check_src_path(src_path, component_name): | |
| """Returns whether src_path has any import errors.""" | |
| sub_modules = [] | |
| submodules_have_errors = False | |
| for dir in os.listdir(src_path): | |
| # If a directory in 'src' is capitalized, treat it as a separate module. | |
| if os.path.isdir(os.path.join(src_path, dir)) and dir[0].isupper(): | |
| sub_modules.append(dir) | |
| check_successful = check_path(os.path.join(src_path, dir)) | |
| submodules_have_errors = submodules_have_errors or check_successful | |
| src_has_errors = check_path(src_path, | |
| checker=ImportChecker.src_checker(component_name), | |
| excludes=sub_modules) | |
| return src_has_errors or submodules_have_errors | |
| def check_path(path, checker=ImportChecker.general_checker(), excludes=()): | |
| """Returns whether imports in files at path have import errors.""" | |
| has_errors = False | |
| files = list_objc_files(path, skip_dirs=excludes) | |
| for f in files: | |
| check_context = CheckContext(checked_file=os.path.basename(f), | |
| module_files=files) | |
| imports = imports_in_file(os.path.join(path, f)) | |
| for import_str in imports: | |
| error = checker.check_import(import_str, check_context) | |
| if error: | |
| print 'ERROR:', error | |
| has_errors = True | |
| return has_errors | |
| def main(args): | |
| if len(args) < 1: | |
| print 'ERROR: Component path should be passed as an input parameter.' | |
| sys.exit(-1) | |
| component_path = os.path.normpath(args[0]) | |
| component_name = os.path.basename(component_path) | |
| src_path = os.path.join(component_path, 'src') | |
| examples_path = os.path.join(component_path, 'examples') | |
| has_errors = check_src_path(src_path, component_name) or \ | |
| check_path(examples_path) | |
| sys.exit(-1 if has_errors else 0) | |
| if __name__ == '__main__': | |
| main(sys.argv[1:]) |