1# Copyright 2021 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Common Utils.""" 16 17# pylint: disable=g-importing-member 18from dataclasses import dataclass 19from pathlib import Path 20from pathlib import PurePath 21import sys 22from typing import List 23from typing import Set 24 25# pylint: disable=g-import-not-at-top 26try: 27 from git import Blob 28 from git import Commit 29 from git import Tree 30except ModuleNotFoundError: 31 print( 32 'ERROR: Please install GitPython by `pip3 install GitPython`.', 33 file=sys.stderr) 34 exit(1) 35 36THIS_DIR = Path(__file__).resolve().parent 37LIBCORE_DIR = THIS_DIR.parent.parent.resolve() 38 39UPSTREAM_CLASS_PATHS = [ 40 'jdk/src/share/classes/', 41 'src/java.base/share/classes/', 42 'src/java.base/linux/classes/', 43 'src/java.base/unix/classes/', 44 'src/java.sql/share/classes/', 45 'src/java.logging/share/classes/', 46 'src/java.prefs/share/classes/', 47 'src/java.net/share/classes/', 48] 49 50UPSTREAM_TEST_PATHS = [ 51 'jdk/test/', 52 'test/jdk/', 53] 54 55UPSTREAM_SEARCH_PATHS = UPSTREAM_CLASS_PATHS + UPSTREAM_TEST_PATHS 56 57OJLUNI_JAVA_BASE_PATH = 'ojluni/src/main/java/' 58OJLUNI_TEST_PATH = 'ojluni/src/' 59 60 61@dataclass 62class ExpectedUpstreamEntry: 63 """A map entry in the EXPECTED_UPSTREAM file.""" 64 dst_path: str # destination path 65 git_ref: str # a git reference to an upstream commit 66 src_path: str # source path in the commit pointed by the git_ref 67 comment_lines: str = '' # The comment lines above the entry line 68 69 70class ExpectedUpstreamFile: 71 """A file object representing the EXPECTED_UPSTREAM file.""" 72 73 def __init__(self, file_path: str = LIBCORE_DIR / 'EXPECTED_UPSTREAM'): 74 self.path = Path(file_path) 75 76 def read_all_entries(self) -> List[ExpectedUpstreamEntry]: 77 """Read all entries from the file.""" 78 result: List[ExpectedUpstreamEntry] = [] 79 with self.path.open() as file: 80 comment_lines = '' # Store the comment lines in the next entry 81 for line in file: 82 stripped = line.strip() 83 # Ignore empty lines and comments starting with '#' 84 if not stripped or stripped.startswith('#'): 85 comment_lines += line 86 continue 87 88 entry = self.parse_line(stripped, comment_lines) 89 result.append(entry) 90 comment_lines = '' 91 92 return result 93 94 def write_all_entries(self, entries: List[ExpectedUpstreamEntry]) -> None: 95 """Write all entries into the file.""" 96 with self.path.open('w') as file: 97 for e in entries: 98 file.write(e.comment_lines) 99 file.write(','.join([e.dst_path, e.git_ref, e.src_path])) 100 file.write('\n') 101 102 def write_new_entry(self, entry: ExpectedUpstreamEntry, 103 entries: List[ExpectedUpstreamEntry] = None) -> None: 104 if entries is None: 105 entries = self.read_all_entries() 106 107 entries.append(entry) 108 self.sort_and_write_all_entries(entries) 109 110 def sort_and_write_all_entries(self, 111 entries: List[ExpectedUpstreamEntry]) -> None: 112 header = entries[0].comment_lines 113 entries[0].comment_lines = '' 114 entries.sort(key=lambda e: e.dst_path) 115 # Keep the header above the first entry 116 entries[0].comment_lines = header + entries[0].comment_lines 117 self.write_all_entries(entries) 118 119 @staticmethod 120 def parse_line(line: str, comment_lines: str) -> ExpectedUpstreamEntry: 121 items = line.split(',') 122 size = len(items) 123 if size != 3: 124 raise ValueError( 125 f"The size must be 3, but is {size}. The line is '{line}'") 126 127 return ExpectedUpstreamEntry(items[0], items[1], items[2], comment_lines) 128 129 130class OjluniFinder: 131 """Finder for java classes or ojluni/ paths.""" 132 133 def __init__(self, existing_paths: List[str]): 134 self.existing_paths = existing_paths 135 136 @staticmethod 137 def translate_from_class_name_to_ojluni_path(class_or_path: str) -> str: 138 """Returns a ojluni path from a class name.""" 139 # if it contains '/', then it's a path 140 if '/' in class_or_path: 141 return class_or_path 142 143 base_path = OJLUNI_TEST_PATH if class_or_path.startswith( 144 'test.') else OJLUNI_JAVA_BASE_PATH 145 146 relative_path = class_or_path.replace('.', '/') 147 return f'{base_path}{relative_path}.java' 148 149 def match_path_prefix(self, input_path: str) -> Set[str]: 150 """Returns a set of existing file paths matching the given partial path.""" 151 path_matches = list( 152 filter(lambda path: path.startswith(input_path), self.existing_paths)) 153 result_set: Set[str] = set() 154 # if it's found, just return the result 155 if input_path in path_matches: 156 result_set.add(input_path) 157 else: 158 input_ojluni_path = PurePath(input_path) 159 # the input ends with '/', the autocompletion result contain the children 160 # instead of the matching the prefix in its parent directory 161 input_path_parent_or_self = input_ojluni_path 162 if not input_path.endswith('/'): 163 input_path_parent_or_self = input_path_parent_or_self.parent 164 n_parts = len(input_path_parent_or_self.parts) 165 for match in path_matches: 166 path = PurePath(match) 167 # path.parts[n_parts] should not exceed the index and should be 168 # a valid child path because input_path_parent_or_self must be a 169 # valid directory 170 child = list(path.parts)[n_parts] 171 result = (input_path_parent_or_self / child).as_posix() 172 # if result is not exact, the result represents a directory. 173 if result != match: 174 result += '/' 175 result_set.add(result) 176 177 return result_set 178 179 def match_classname_prefix(self, input_class_name: str) -> List[str]: 180 """Returns a list of package / class names given the partial class name.""" 181 # If '/' exists, it's probably a path, not a partial class name 182 if '/' in input_class_name: 183 return [] 184 185 result_list = [] 186 partial_relative_path = input_class_name.replace('.', '/') 187 for base_path in [OJLUNI_JAVA_BASE_PATH, OJLUNI_TEST_PATH]: 188 partial_ojluni_path = base_path + partial_relative_path 189 result_paths = self.match_path_prefix(partial_ojluni_path) 190 # pylint: disable=cell-var-from-loop 191 result_list.extend( 192 map(lambda path: convert_path_to_java_class_name(path, base_path), 193 list(result_paths))) 194 195 return result_list 196 197 198class OpenjdkFinder: 199 """Finder for java classes or paths in a upstream OpenJDK commit.""" 200 201 def __init__(self, commit: Commit): 202 self.commit = commit 203 204 @staticmethod 205 def translate_src_path_to_ojluni_path(src_path: str) -> str: 206 """Returns None if src_path isn't in a known source directory.""" 207 relative_path = None 208 for base_path in UPSTREAM_TEST_PATHS: 209 if src_path.startswith(base_path): 210 length = len(base_path) 211 relative_path = src_path[length:] 212 break 213 214 if relative_path: 215 return f'{OJLUNI_TEST_PATH}test/{relative_path}' 216 217 for base_path in UPSTREAM_CLASS_PATHS: 218 if src_path.startswith(base_path): 219 length = len(base_path) 220 relative_path = src_path[length:] 221 break 222 223 if relative_path: 224 return f'{OJLUNI_JAVA_BASE_PATH}{relative_path}' 225 226 return None 227 228 def find_src_path_from_classname(self, class_or_path: str) -> str: 229 """Finds a valid source path given a valid class name or path.""" 230 # if it contains '/', then it's a path 231 if '/' in class_or_path: 232 if self.has_file(class_or_path): 233 return class_or_path 234 else: 235 return None 236 237 relative_path = class_or_path.replace('.', '/') 238 src_path = None 239 for base_path in UPSTREAM_SEARCH_PATHS: 240 full_path = f'{base_path}{relative_path}.java' 241 if self.has_file(full_path): 242 src_path = full_path 243 break 244 245 return src_path 246 247 def get_search_paths(self) -> List[str]: 248 return UPSTREAM_SEARCH_PATHS 249 250 def find_src_path_from_ojluni_path(self, ojluni_path: str) -> str: 251 """Returns a source path that guessed from the ojluni_path.""" 252 base_paths = None 253 relative_path = None 254 255 TEST_PATH = OJLUNI_TEST_PATH + 'test/' 256 if ojluni_path.startswith(OJLUNI_JAVA_BASE_PATH): 257 base_paths = UPSTREAM_CLASS_PATHS 258 length = len(OJLUNI_JAVA_BASE_PATH) 259 relative_path = ojluni_path[length:] 260 elif ojluni_path.startswith(TEST_PATH): 261 base_paths = UPSTREAM_TEST_PATHS 262 length = len(TEST_PATH) 263 relative_path = ojluni_path[length:] 264 else: 265 return None 266 267 for base_path in base_paths: 268 full_path = base_path + relative_path 269 if self.has_file(full_path): 270 return full_path 271 272 return None 273 274 def match_path_prefix(self, input_path: str) -> List[str]: 275 """Returns a list of source paths matching the given partial string.""" 276 result_list = [] 277 278 search_tree = self.commit.tree 279 path_obj = PurePath(input_path) 280 is_exact = self.has_file(path_obj.as_posix()) 281 is_directory_path = input_path.endswith('/') 282 exact_obj = search_tree[path_obj.as_posix()] if is_exact else None 283 search_word = '' 284 if is_exact and isinstance(exact_obj, Blob): 285 # an exact file path 286 result_list.append(input_path) 287 return result_list 288 elif is_directory_path: 289 # an exact directory path and can't be a prefix directory name. 290 if is_exact: 291 search_tree = exact_obj 292 else: 293 # Such path doesn't exist, and thus returns empty list 294 return result_list 295 elif len(path_obj.parts) >= 2 and not is_directory_path: 296 parent_path = path_obj.parent.as_posix() 297 if self.has_file(parent_path): 298 search_tree = search_tree[parent_path] 299 search_word = path_obj.name 300 else: 301 # Return empty list because no such path is found 302 return result_list 303 else: 304 search_word = input_path 305 306 for tree in search_tree.trees: 307 tree_path = PurePath(tree.path) 308 if tree_path.name.startswith(search_word): 309 # Append '/' to indicate directory type. If the result has this item 310 # only, shell should auto-fill the input, and thus 311 # next tabbing in shell should fall into the above condition 312 # `is_exact and input_path.endswith('/')` and will search in the child 313 # tree. 314 result_path = tree.path + '/' 315 result_list.append(result_path) 316 317 for blob in search_tree.blobs: 318 blob_path = PurePath(blob.path) 319 if blob_path.name.startswith(search_word): 320 result_list.append(blob.path) 321 322 return result_list 323 324 def match_classname_prefix(self, input_class_name: str) -> List[str]: 325 """Return a list of package / class names from given commit and input.""" 326 # If '/' exists, it's probably a path, not a class name. 327 if '/' in input_class_name: 328 return [] 329 330 result_list = [] 331 for base_path in UPSTREAM_SEARCH_PATHS: 332 base_len = len(base_path) 333 path = base_path + input_class_name.replace('.', '/') 334 path_results = self.match_path_prefix(path) 335 for p in path_results: 336 relative_path = p[base_len:] 337 if relative_path.endswith('.java'): 338 relative_path = relative_path[0:-5] 339 result_list.append(relative_path.replace('/', '.')) 340 341 return result_list 342 343 def has_file(self, path: str) -> bool: 344 """Returns True if the directory / file exists in the tree.""" 345 return has_file_in_tree(path, self.commit.tree) 346 347 348def convert_path_to_java_class_name(path: str, base_path: str) -> str: 349 base_len = len(base_path) 350 result = path[base_len:] 351 if result.endswith('.java'): 352 result = result[0:-5] 353 result = result.replace('/', '.') 354 return result 355 356 357def has_file_in_tree(path: str, tree: Tree) -> bool: 358 """Returns True if the directory / file exists in the tree.""" 359 try: 360 # pylint: disable=pointless-statement 361 tree[path] 362 return True 363 except KeyError: 364 return False 365