1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 """Checks Java files for illegal imports.""" 6 7 import codecs 8 import os 9 import re 10 11 import results 12 from rules import Rule 13 14 15 class JavaChecker(object): 16 """Import checker for Java files. 17 18 The CheckFile method uses real filesystem paths, but Java imports work in 19 terms of package names. To deal with this, we have an extra "prescan" pass 20 that reads all the .java files and builds a mapping of class name -> filepath. 21 In CheckFile, we convert each import statement into a real filepath, and check 22 that against the rules in the DEPS files. 23 24 Note that in Java you can always use classes in the same directory without an 25 explicit import statement, so these imports can't be blocked with DEPS files. 26 But that shouldn't be a problem, because same-package imports are pretty much 27 always correct by definition. (If we find a case where this is *not* correct, 28 it probably means the package is too big and needs to be split up.) 29 30 Properties: 31 _classmap: dict of fully-qualified Java class name -> filepath 32 """ 33 34 EXTENSIONS = ['.java'] 35 36 def __init__(self, base_directory, verbose): 37 self._base_directory = base_directory 38 self._verbose = verbose 39 self._classmap = {} 40 self._PrescanFiles() 41 42 def _PrescanFiles(self): 43 for root, dirs, files in os.walk(self._base_directory): 44 # Skip unwanted subdirectories. TODO(husky): it would be better to do 45 # this via the skip_child_includes flag in DEPS files. Maybe hoist this 46 # prescan logic into checkdeps.py itself? 47 for d in dirs: 48 # Skip hidden directories. 49 if d.startswith('.'): 50 dirs.remove(d) 51 # Skip the "out" directory, as dealing with generated files is awkward. 52 # We don't want paths like "out/Release/lib.java" in our DEPS files. 53 # TODO(husky): We need some way of determining the "real" path to 54 # a generated file -- i.e., where it would be in source control if 55 # it weren't generated. 56 if d == 'out': 57 dirs.remove(d) 58 # Skip third-party directories. 59 if d == 'third_party': 60 dirs.remove(d) 61 for f in files: 62 if f.endswith('.java'): 63 self._PrescanFile(os.path.join(root, f)) 64 65 def _PrescanFile(self, filepath): 66 if self._verbose: 67 print 'Prescanning: ' + filepath 68 with codecs.open(filepath, encoding='utf-8') as f: 69 short_class_name, _ = os.path.splitext(os.path.basename(filepath)) 70 for line in f: 71 for package in re.findall('^package ([\w\.]+);', line): 72 full_class_name = package + '.' + short_class_name 73 if full_class_name in self._classmap: 74 print 'WARNING: multiple definitions of %s:' % full_class_name 75 print ' ' + filepath 76 print ' ' + self._classmap[full_class_name] 77 print 78 else: 79 self._classmap[full_class_name] = filepath 80 return 81 print 'WARNING: no package definition found in %s' % filepath 82 83 def CheckFile(self, rules, filepath): 84 if self._verbose: 85 print 'Checking: ' + filepath 86 87 dependee_status = results.DependeeStatus(filepath) 88 with codecs.open(filepath, encoding='utf-8') as f: 89 for line in f: 90 for clazz in re.findall('^import\s+(?:static\s+)?([\w\.]+)\s*;', line): 91 if clazz not in self._classmap: 92 # Importing a class from outside the Chromium tree. That's fine -- 93 # it's probably a Java or Android system class. 94 continue 95 include_path = os.path.relpath( 96 self._classmap[clazz], self._base_directory) 97 # Convert Windows paths to Unix style, as used in DEPS files. 98 include_path = include_path.replace(os.path.sep, '/') 99 rule = rules.RuleApplyingTo(include_path, filepath) 100 if rule.allow == Rule.DISALLOW: 101 dependee_status.AddViolation( 102 results.DependencyViolation(include_path, rule, rules)) 103 if '{' in line: 104 # This is code, so we're finished reading imports for this file. 105 break 106 107 return dependee_status 108