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.
5 """Checks Java files for illegal imports."""
12 from rules import Rule
15 class JavaChecker(object):
16 """Import checker for Java files.
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.
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.)
31 _classmap: dict of fully-qualified Java class name -> filepath
34 EXTENSIONS = ['.java']
36 # This regular expression will be used to extract filenames from import
38 _EXTRACT_IMPORT_PATH = re.compile('^import\s+(?:static\s+)?([\w\.]+)\s*;')
40 def __init__(self, base_directory, verbose, added_imports=None,
41 allow_multiple_definitions=None):
42 self._base_directory = base_directory
43 self._verbose = verbose
45 self._allow_multiple_definitions = allow_multiple_definitions or []
47 added_classset = self._PrescanImportFiles(added_imports)
48 self._PrescanFiles(added_classset)
50 def _GetClassFullName(self, filepath):
51 """Get the full class name of a file with package name."""
52 if not os.path.isfile(filepath):
54 with codecs.open(filepath, encoding='utf-8') as f:
55 short_class_name, _ = os.path.splitext(os.path.basename(filepath))
57 for package in re.findall('^package\s+([\w\.]+);', line):
58 return package + '.' + short_class_name
60 def _IgnoreDir(self, d):
61 # Skip hidden directories.
64 # Skip the "out" directory, as dealing with generated files is awkward.
65 # We don't want paths like "out/Release/lib.java" in our DEPS files.
66 # TODO(husky): We need some way of determining the "real" path to
67 # a generated file -- i.e., where it would be in source control if
68 # it weren't generated.
69 if d.startswith('out') or d in ('xcodebuild', 'AndroidStudioDefault',):
71 # Skip third-party directories.
72 if d in ('third_party', 'ThirdParty'):
76 def _PrescanFiles(self, added_classset):
77 for root, dirs, files in os.walk(self._base_directory.encode('utf-8')):
78 # Skip unwanted subdirectories. TODO(husky): it would be better to do
79 # this via the skip_child_includes flag in DEPS files. Maybe hoist this
80 # prescan logic into checkdeps.py itself?
81 dirs[:] = [d for d in dirs if not self._IgnoreDir(d)]
83 if f.endswith('.java'):
84 self._PrescanFile(os.path.join(root, f), added_classset)
86 def _PrescanImportFiles(self, added_imports):
87 """Build a set of fully-qualified class affected by this patch.
89 Prescan imported files and build classset to collect full class names
90 with package name. This includes both changed files as well as changed
94 added_imports : ((file_path, (import_line, import_line, ...), ...)
97 A set of full class names with package name of imported files.
100 for filepath, changed_lines in (added_imports or []):
101 if not self.ShouldCheck(filepath):
103 full_class_name = self._GetClassFullName(filepath)
105 classset.add(full_class_name)
106 for line in changed_lines:
107 found_item = self._EXTRACT_IMPORT_PATH.match(line)
109 classset.add(found_item.group(1))
112 def _PrescanFile(self, filepath, added_classset):
114 print 'Prescanning: ' + filepath
115 full_class_name = self._GetClassFullName(filepath)
117 if full_class_name in self._classmap:
118 if self._verbose or full_class_name in added_classset:
119 if not any(re.match(i, filepath) for i in
120 self._allow_multiple_definitions):
121 print 'WARNING: multiple definitions of %s:' % full_class_name
123 print ' ' + self._classmap[full_class_name]
126 self._classmap[full_class_name] = filepath
128 print 'WARNING: no package definition found in %s' % filepath
130 def CheckLine(self, rules, line, filepath, fail_on_temp_allow=False):
131 """Checks the given line with the given rule set.
133 Returns a tuple (is_import, dependency_violation) where
134 is_import is True only if the line is an import
135 statement, and dependency_violation is an instance of
136 results.DependencyViolation if the line violates a rule, or None
139 found_item = self._EXTRACT_IMPORT_PATH.match(line)
141 return False, None # Not a match
142 clazz = found_item.group(1)
143 if clazz not in self._classmap:
144 # Importing a class from outside the Chromium tree. That's fine --
145 # it's probably a Java or Android system class.
147 import_path = os.path.relpath(
148 self._classmap[clazz], self._base_directory)
149 # Convert Windows paths to Unix style, as used in DEPS files.
150 import_path = import_path.replace(os.path.sep, '/')
151 rule = rules.RuleApplyingTo(import_path, filepath)
152 if (rule.allow == Rule.DISALLOW or
153 (fail_on_temp_allow and rule.allow == Rule.TEMP_ALLOW)):
154 return True, results.DependencyViolation(import_path, rule, rules)
157 def CheckFile(self, rules, filepath):
159 print 'Checking: ' + filepath
161 dependee_status = results.DependeeStatus(filepath)
162 with codecs.open(filepath, encoding='utf-8') as f:
164 is_import, violation = self.CheckLine(rules, line, filepath)
166 dependee_status.AddViolation(violation)
168 # This is code, so we're finished reading imports for this file.
171 return dependee_status
174 def IsJavaFile(filepath):
175 """Returns True if the given path ends in the extensions
176 handled by this checker.
178 return os.path.splitext(filepath)[1] in JavaChecker.EXTENSIONS
180 def ShouldCheck(self, file_path):
181 """Check if the new import file path should be presubmit checked.
184 file_path: file path to be checked
187 bool: True if the file should be checked; False otherwise.
189 return self.IsJavaFile(file_path)