java_checker.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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. """Checks Java files for illegal imports."""
  5. import codecs
  6. import os
  7. import re
  8. import results
  9. from rules import Rule
  10. class JavaChecker(object):
  11. """Import checker for Java files.
  12. The CheckFile method uses real filesystem paths, but Java imports work in
  13. terms of package names. To deal with this, we have an extra "prescan" pass
  14. that reads all the .java files and builds a mapping of class name -> filepath.
  15. In CheckFile, we convert each import statement into a real filepath, and check
  16. that against the rules in the DEPS files.
  17. Note that in Java you can always use classes in the same directory without an
  18. explicit import statement, so these imports can't be blocked with DEPS files.
  19. But that shouldn't be a problem, because same-package imports are pretty much
  20. always correct by definition. (If we find a case where this is *not* correct,
  21. it probably means the package is too big and needs to be split up.)
  22. Properties:
  23. _classmap: dict of fully-qualified Java class name -> filepath
  24. """
  25. EXTENSIONS = ['.java']
  26. # This regular expression will be used to extract filenames from import
  27. # statements.
  28. _EXTRACT_IMPORT_PATH = re.compile(r'^import\s+(?:static\s+)?([\w\.]+)\s*;')
  29. def __init__(self, base_directory, verbose, added_imports=None,
  30. allow_multiple_definitions=None):
  31. self._base_directory = base_directory
  32. self._verbose = verbose
  33. self._classmap = {}
  34. self._allow_multiple_definitions = allow_multiple_definitions or []
  35. if added_imports:
  36. added_classset = self._PrescanImportFiles(added_imports)
  37. self._PrescanFiles(added_classset)
  38. def _GetClassFullName(self, filepath):
  39. """Get the full class name of a file with package name."""
  40. if not os.path.isfile(filepath):
  41. return None
  42. with codecs.open(filepath, encoding='utf-8') as f:
  43. short_class_name, _ = os.path.splitext(os.path.basename(filepath))
  44. for line in f:
  45. for package in re.findall(r'^package\s+([\w\.]+);', line):
  46. return package + '.' + short_class_name
  47. def _IgnoreDir(self, d):
  48. # Skip hidden directories.
  49. if d.startswith('.'):
  50. return True
  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.startswith('out') or d in ('xcodebuild', 'AndroidStudioDefault',
  57. 'libassistant',):
  58. return True
  59. # Skip third-party directories.
  60. if d in ('third_party', 'ThirdParty'):
  61. return True
  62. return False
  63. def _PrescanFiles(self, added_classset):
  64. for root, dirs, files in os.walk(self._base_directory):
  65. # Skip unwanted subdirectories. TODO(husky): it would be better to do
  66. # this via the skip_child_includes flag in DEPS files. Maybe hoist this
  67. # prescan logic into checkdeps.py itself?
  68. # Modify dirs in-place with slice assignment to avoid recursing into them.
  69. dirs[:] = [d for d in dirs if not self._IgnoreDir(d)]
  70. for f in files:
  71. if f.endswith('.java'):
  72. self._PrescanFile(os.path.join(root, f), added_classset)
  73. def _PrescanImportFiles(self, added_imports):
  74. """Build a set of fully-qualified class affected by this patch.
  75. Prescan imported files and build classset to collect full class names
  76. with package name. This includes both changed files as well as changed
  77. imports.
  78. Args:
  79. added_imports : ((file_path, (import_line, import_line, ...), ...)
  80. Return:
  81. A set of full class names with package name of imported files.
  82. """
  83. classset = set()
  84. for filepath, changed_lines in (added_imports or []):
  85. if not self.ShouldCheck(filepath):
  86. continue
  87. full_class_name = self._GetClassFullName(filepath)
  88. if full_class_name:
  89. classset.add(full_class_name)
  90. for line in changed_lines:
  91. found_item = self._EXTRACT_IMPORT_PATH.match(line)
  92. if found_item:
  93. classset.add(found_item.group(1))
  94. return classset
  95. def _PrescanFile(self, filepath, added_classset):
  96. if self._verbose:
  97. print('Prescanning: ' + filepath)
  98. full_class_name = self._GetClassFullName(filepath)
  99. if full_class_name:
  100. if full_class_name in self._classmap:
  101. if self._verbose or full_class_name in added_classset:
  102. if not any(re.match(i, filepath) for i in
  103. self._allow_multiple_definitions):
  104. print('WARNING: multiple definitions of %s:' % full_class_name)
  105. print(' ' + filepath)
  106. print(' ' + self._classmap[full_class_name])
  107. print()
  108. # Prefer the public repo when multiple matches are found.
  109. if self._classmap[full_class_name].startswith(
  110. os.path.join(self._base_directory, 'clank')):
  111. self._classmap[full_class_name] = filepath
  112. else:
  113. self._classmap[full_class_name] = filepath
  114. elif self._verbose:
  115. print('WARNING: no package definition found in %s' % filepath)
  116. def CheckLine(self, rules, line, filepath, fail_on_temp_allow=False):
  117. """Checks the given line with the given rule set.
  118. Returns a tuple (is_import, dependency_violation) where
  119. is_import is True only if the line is an import
  120. statement, and dependency_violation is an instance of
  121. results.DependencyViolation if the line violates a rule, or None
  122. if it does not.
  123. """
  124. found_item = self._EXTRACT_IMPORT_PATH.match(line)
  125. if not found_item:
  126. return False, None # Not a match
  127. clazz = found_item.group(1)
  128. if clazz not in self._classmap:
  129. # Importing a class from outside the Chromium tree. That's fine --
  130. # it's probably a Java or Android system class.
  131. return True, None
  132. import_path = os.path.relpath(
  133. self._classmap[clazz], self._base_directory)
  134. # Convert Windows paths to Unix style, as used in DEPS files.
  135. import_path = import_path.replace(os.path.sep, '/')
  136. rule = rules.RuleApplyingTo(import_path, filepath)
  137. if (rule.allow == Rule.DISALLOW or
  138. (fail_on_temp_allow and rule.allow == Rule.TEMP_ALLOW)):
  139. return True, results.DependencyViolation(import_path, rule, rules)
  140. return True, None
  141. def CheckFile(self, rules, filepath):
  142. if self._verbose:
  143. print('Checking: ' + filepath)
  144. dependee_status = results.DependeeStatus(filepath)
  145. with codecs.open(filepath, encoding='utf-8') as f:
  146. for line in f:
  147. is_import, violation = self.CheckLine(rules, line, filepath)
  148. if violation:
  149. dependee_status.AddViolation(violation)
  150. if '{' in line:
  151. # This is code, so we're finished reading imports for this file.
  152. break
  153. return dependee_status
  154. @staticmethod
  155. def IsJavaFile(filepath):
  156. """Returns True if the given path ends in the extensions
  157. handled by this checker.
  158. """
  159. return os.path.splitext(filepath)[1] in JavaChecker.EXTENSIONS
  160. def ShouldCheck(self, file_path):
  161. """Check if the new import file path should be presubmit checked.
  162. Args:
  163. file_path: file path to be checked
  164. Return:
  165. bool: True if the file should be checked; False otherwise.
  166. """
  167. return self.IsJavaFile(file_path)