• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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