#!/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 , 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:])