|  | # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  |  | 
|  | """Checks Java files for illegal imports.""" | 
|  |  | 
|  | import codecs | 
|  | import os | 
|  | import re | 
|  |  | 
|  | import results | 
|  | from rules import Rule | 
|  |  | 
|  |  | 
|  | class JavaChecker(object): | 
|  | """Import checker for Java files. | 
|  |  | 
|  | The CheckFile method uses real filesystem paths, but Java imports work in | 
|  | terms of package names. To deal with this, we have an extra "prescan" pass | 
|  | that reads all the .java files and builds a mapping of class name -> filepath. | 
|  | In CheckFile, we convert each import statement into a real filepath, and check | 
|  | that against the rules in the DEPS files. | 
|  |  | 
|  | Note that in Java you can always use classes in the same directory without an | 
|  | explicit import statement, so these imports can't be blocked with DEPS files. | 
|  | But that shouldn't be a problem, because same-package imports are pretty much | 
|  | always correct by definition. (If we find a case where this is *not* correct, | 
|  | it probably means the package is too big and needs to be split up.) | 
|  |  | 
|  | Properties: | 
|  | _classmap: dict of fully-qualified Java class name -> filepath | 
|  | """ | 
|  |  | 
|  | EXTENSIONS = ['.java'] | 
|  |  | 
|  | # This regular expression will be used to extract filenames from import | 
|  | # statements. | 
|  | _EXTRACT_IMPORT_PATH = re.compile('^import\s+(?:static\s+)?([\w\.]+)\s*;') | 
|  |  | 
|  | def __init__(self, base_directory, verbose, added_imports=None, | 
|  | allow_multiple_definitions=None): | 
|  | self._base_directory = base_directory | 
|  | self._verbose = verbose | 
|  | self._classmap = {} | 
|  | self._allow_multiple_definitions = allow_multiple_definitions or [] | 
|  | if added_imports: | 
|  | added_classset = self._PrescanImportFiles(added_imports) | 
|  | self._PrescanFiles(added_classset) | 
|  |  | 
|  | def _GetClassFullName(self, filepath): | 
|  | """Get the full class name of a file with package name.""" | 
|  | if not os.path.isfile(filepath): | 
|  | return None | 
|  | with codecs.open(filepath, encoding='utf-8') as f: | 
|  | short_class_name, _ = os.path.splitext(os.path.basename(filepath)) | 
|  | for line in f: | 
|  | for package in re.findall('^package\s+([\w\.]+);', line): | 
|  | return package + '.' + short_class_name | 
|  |  | 
|  | def _IgnoreDir(self, d): | 
|  | # Skip hidden directories. | 
|  | if d.startswith('.'): | 
|  | return True | 
|  | # Skip the "out" directory, as dealing with generated files is awkward. | 
|  | # We don't want paths like "out/Release/lib.java" in our DEPS files. | 
|  | # TODO(husky): We need some way of determining the "real" path to | 
|  | # a generated file -- i.e., where it would be in source control if | 
|  | # it weren't generated. | 
|  | if d.startswith('out') or d in ('xcodebuild',): | 
|  | return True | 
|  | # Skip third-party directories. | 
|  | if d in ('third_party', 'ThirdParty'): | 
|  | return True | 
|  | return False | 
|  |  | 
|  | def _PrescanFiles(self, added_classset): | 
|  | for root, dirs, files in os.walk(self._base_directory): | 
|  | # Skip unwanted subdirectories. TODO(husky): it would be better to do | 
|  | # this via the skip_child_includes flag in DEPS files. Maybe hoist this | 
|  | # prescan logic into checkdeps.py itself? | 
|  | dirs[:] = [d for d in dirs if not self._IgnoreDir(d)] | 
|  | for f in files: | 
|  | if f.endswith('.java'): | 
|  | self._PrescanFile(os.path.join(root, f), added_classset) | 
|  |  | 
|  | def _PrescanImportFiles(self, added_imports): | 
|  | """Build a set of fully-qualified class affected by this patch. | 
|  |  | 
|  | Prescan imported files and build classset to collect full class names | 
|  | with package name. This includes both changed files as well as changed imports. | 
|  |  | 
|  | Args: | 
|  | added_imports : ((file_path, (import_line, import_line, ...), ...) | 
|  |  | 
|  | Return: | 
|  | A set of full class names with package name of imported files. | 
|  | """ | 
|  | classset = set() | 
|  | for filepath, changed_lines in (added_imports or []): | 
|  | if not self.ShouldCheck(filepath): | 
|  | continue | 
|  | full_class_name = self._GetClassFullName(filepath) | 
|  | if full_class_name: | 
|  | classset.add(full_class_name) | 
|  | for line in changed_lines: | 
|  | found_item = self._EXTRACT_IMPORT_PATH.match(line) | 
|  | if found_item: | 
|  | classset.add(found_item.group(1)) | 
|  | return classset | 
|  |  | 
|  | def _PrescanFile(self, filepath, added_classset): | 
|  | if self._verbose: | 
|  | print 'Prescanning: ' + filepath | 
|  | full_class_name = self._GetClassFullName(filepath) | 
|  | if full_class_name: | 
|  | if full_class_name in self._classmap: | 
|  | if self._verbose or full_class_name in added_classset: | 
|  | if not any((re.match(i, filepath) for i in self._allow_multiple_definitions)): | 
|  | print 'WARNING: multiple definitions of %s:' % full_class_name | 
|  | print '    ' + filepath | 
|  | print '    ' + self._classmap[full_class_name] | 
|  | print | 
|  | else: | 
|  | self._classmap[full_class_name] = filepath | 
|  | elif self._verbose: | 
|  | print 'WARNING: no package definition found in %s' % filepath | 
|  |  | 
|  | def CheckLine(self, rules, line, filepath, fail_on_temp_allow=False): | 
|  | """Checks the given line with the given rule set. | 
|  |  | 
|  | Returns a tuple (is_import, dependency_violation) where | 
|  | is_import is True only if the line is an import | 
|  | statement, and dependency_violation is an instance of | 
|  | results.DependencyViolation if the line violates a rule, or None | 
|  | if it does not. | 
|  | """ | 
|  | found_item = self._EXTRACT_IMPORT_PATH.match(line) | 
|  | if not found_item: | 
|  | return False, None  # Not a match | 
|  | clazz = found_item.group(1) | 
|  | if clazz not in self._classmap: | 
|  | # Importing a class from outside the Chromium tree. That's fine -- | 
|  | # it's probably a Java or Android system class. | 
|  | return True, None | 
|  | import_path = os.path.relpath( | 
|  | self._classmap[clazz], self._base_directory) | 
|  | # Convert Windows paths to Unix style, as used in DEPS files. | 
|  | import_path = import_path.replace(os.path.sep, '/') | 
|  | rule = rules.RuleApplyingTo(import_path, filepath) | 
|  | if (rule.allow == Rule.DISALLOW or | 
|  | (fail_on_temp_allow and rule.allow == Rule.TEMP_ALLOW)): | 
|  | return True, results.DependencyViolation(import_path, rule, rules) | 
|  | return True, None | 
|  |  | 
|  | def CheckFile(self, rules, filepath): | 
|  | if self._verbose: | 
|  | print 'Checking: ' + filepath | 
|  |  | 
|  | dependee_status = results.DependeeStatus(filepath) | 
|  | with codecs.open(filepath, encoding='utf-8') as f: | 
|  | for line in f: | 
|  | is_import, violation = self.CheckLine(rules, line, filepath) | 
|  | if violation: | 
|  | dependee_status.AddViolation(violation) | 
|  | if '{' in line: | 
|  | # This is code, so we're finished reading imports for this file. | 
|  | break | 
|  |  | 
|  | return dependee_status | 
|  |  | 
|  | @staticmethod | 
|  | def IsJavaFile(filepath): | 
|  | """Returns True if the given path ends in the extensions | 
|  | handled by this checker. | 
|  | """ | 
|  | return os.path.splitext(filepath)[1] in JavaChecker.EXTENSIONS | 
|  |  | 
|  | def ShouldCheck(self, file_path): | 
|  | """Check if the new import file path should be presubmit checked. | 
|  |  | 
|  | Args: | 
|  | file_path: file path to be checked | 
|  |  | 
|  | Return: | 
|  | bool: True if the file should be checked; False otherwise. | 
|  | """ | 
|  | return self.IsJavaFile(file_path) |