fixup! Upload upstream chromium 76.0.3809.146
[platform/framework/web/chromium-efl.git] / buildtools / checkdeps / java_checker.py
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   # This regular expression will be used to extract filenames from import
37   # statements.
38   _EXTRACT_IMPORT_PATH = re.compile('^import\s+(?:static\s+)?([\w\.]+)\s*;')
39
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
44     self._classmap = {}
45     self._allow_multiple_definitions = allow_multiple_definitions or []
46     if added_imports:
47       added_classset = self._PrescanImportFiles(added_imports)
48       self._PrescanFiles(added_classset)
49
50   def _GetClassFullName(self, filepath):
51     """Get the full class name of a file with package name."""
52     if not os.path.isfile(filepath):
53       return None
54     with codecs.open(filepath, encoding='utf-8') as f:
55       short_class_name, _ = os.path.splitext(os.path.basename(filepath))
56       for line in f:
57         for package in re.findall('^package\s+([\w\.]+);', line):
58           return package + '.' + short_class_name
59
60   def _IgnoreDir(self, d):
61     # Skip hidden directories.
62     if d.startswith('.'):
63       return True
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',):
70       return True
71     # Skip third-party directories.
72     if d in ('third_party', 'ThirdParty'):
73       return True
74     return False
75
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)]
82       for f in files:
83         if f.endswith('.java'):
84           self._PrescanFile(os.path.join(root, f), added_classset)
85
86   def _PrescanImportFiles(self, added_imports):
87     """Build a set of fully-qualified class affected by this patch.
88
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
91     imports.
92
93     Args:
94       added_imports : ((file_path, (import_line, import_line, ...), ...)
95
96     Return:
97       A set of full class names with package name of imported files.
98     """
99     classset = set()
100     for filepath, changed_lines in (added_imports or []):
101       if not self.ShouldCheck(filepath):
102         continue
103       full_class_name = self._GetClassFullName(filepath)
104       if full_class_name:
105         classset.add(full_class_name)
106       for line in changed_lines:
107         found_item = self._EXTRACT_IMPORT_PATH.match(line)
108         if found_item:
109           classset.add(found_item.group(1))
110     return classset
111
112   def _PrescanFile(self, filepath, added_classset):
113     if self._verbose:
114       print 'Prescanning: ' + filepath
115     full_class_name = self._GetClassFullName(filepath)
116     if full_class_name:
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
122             print '    ' + filepath
123             print '    ' + self._classmap[full_class_name]
124             print
125       else:
126         self._classmap[full_class_name] = filepath
127     elif self._verbose:
128       print 'WARNING: no package definition found in %s' % filepath
129
130   def CheckLine(self, rules, line, filepath, fail_on_temp_allow=False):
131     """Checks the given line with the given rule set.
132
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
137     if it does not.
138     """
139     found_item = self._EXTRACT_IMPORT_PATH.match(line)
140     if not found_item:
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.
146       return True, None
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)
155     return True, None
156
157   def CheckFile(self, rules, filepath):
158     if self._verbose:
159       print 'Checking: ' + filepath
160
161     dependee_status = results.DependeeStatus(filepath)
162     with codecs.open(filepath, encoding='utf-8') as f:
163       for line in f:
164         is_import, violation = self.CheckLine(rules, line, filepath)
165         if violation:
166           dependee_status.AddViolation(violation)
167         if '{' in line:
168           # This is code, so we're finished reading imports for this file.
169           break
170
171     return dependee_status
172
173   @staticmethod
174   def IsJavaFile(filepath):
175     """Returns True if the given path ends in the extensions
176     handled by this checker.
177     """
178     return os.path.splitext(filepath)[1] in JavaChecker.EXTENSIONS
179
180   def ShouldCheck(self, file_path):
181     """Check if the new import file path should be presubmit checked.
182
183     Args:
184       file_path: file path to be checked
185
186     Return:
187       bool: True if the file should be checked; False otherwise.
188     """
189     return self.IsJavaFile(file_path)