print_class_dependencies.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. #!/usr/bin/env python3
  2. # Copyright 2020 The Chromium Authors. All rights reserved.
  3. # Use of this source code is governed by a BSD-style license that can be
  4. # found in the LICENSE file.
  5. """Command-line tool for printing class-level dependencies."""
  6. import argparse
  7. from dataclasses import dataclass
  8. from typing import List, Set, Tuple
  9. import chrome_names
  10. import class_dependency
  11. import graph
  12. import package_dependency
  13. import print_dependencies_helper
  14. import serialization
  15. # Return values of categorize_dependency().
  16. IGNORE = 'ignore'
  17. CLEAR = 'clear'
  18. PRINT = 'print'
  19. @dataclass
  20. class PrintMode:
  21. """Options of how and which dependencies to output."""
  22. inbound: bool
  23. outbound: bool
  24. ignore_modularized: bool
  25. ignore_audited_here: bool
  26. ignore_same_package: bool
  27. fully_qualified: bool
  28. class TargetDependencies:
  29. """Build target dependencies that the set of classes depends on."""
  30. def __init__(self):
  31. # Build targets usable by modularized code that need to be depended on.
  32. self.cleared: Set[str] = set()
  33. # Build targets usable by modularized code that might need to be
  34. # depended on. This happens rarely, when a class dependency is in
  35. # multiple build targets (Android resource .R classes, or due to
  36. # bytecode rewriting).
  37. self.dubious: Set[str] = set()
  38. def update_with_class_node(self, class_node: class_dependency.JavaClass):
  39. if len(class_node.build_targets) == 1:
  40. self.cleared.update(class_node.build_targets)
  41. else:
  42. self.dubious.update(class_node.build_targets)
  43. def merge(self, other: 'TargetDependencies'):
  44. self.cleared.update(other.cleared)
  45. self.dubious.update(other.dubious)
  46. def print(self):
  47. if self.cleared:
  48. print()
  49. print('Cleared dependencies:')
  50. for dep in sorted(self.cleared):
  51. print(dep)
  52. if self.dubious:
  53. print()
  54. print('Dubious dependencies due to classes with multiple build '
  55. 'targets. Only some of these are required:')
  56. for dep in sorted(self.dubious):
  57. print(dep)
  58. INBOUND = 'inbound'
  59. OUTBOUND = 'outbound'
  60. ALLOWED_PREFIXES = {
  61. '//base/',
  62. '//base:',
  63. '//chrome/browser/',
  64. '//components/',
  65. '//content/',
  66. '//ui/',
  67. '//url:',
  68. }
  69. IGNORED_CLASSES = {'org.chromium.base.natives.GEN_JNI'}
  70. def get_class_name_to_display(fully_qualified_name: str,
  71. print_mode: PrintMode) -> str:
  72. if print_mode.fully_qualified:
  73. return fully_qualified_name
  74. else:
  75. return chrome_names.shorten_class(fully_qualified_name)
  76. def get_build_target_name_to_display(build_target: str,
  77. print_mode: PrintMode) -> str:
  78. if print_mode.fully_qualified:
  79. return build_target
  80. else:
  81. return chrome_names.shorten_build_target(build_target)
  82. def is_allowed_target_dependency(build_target: str) -> bool:
  83. return any(build_target.startswith(p) for p in ALLOWED_PREFIXES)
  84. def is_ignored_class_dependency(class_name: str) -> bool:
  85. return class_name in IGNORED_CLASSES
  86. def categorize_dependency(from_class: class_dependency.JavaClass,
  87. to_class: class_dependency.JavaClass,
  88. ignore_modularized: bool, print_mode: PrintMode,
  89. audited_classes: Set[str]) -> str:
  90. """Decides if a class dependency should be printed, cleared, or ignored."""
  91. if is_ignored_class_dependency(to_class.name):
  92. return IGNORE
  93. if ignore_modularized and all(
  94. is_allowed_target_dependency(target)
  95. for target in to_class.build_targets):
  96. return CLEAR
  97. if (print_mode.ignore_same_package
  98. and to_class.package == from_class.package):
  99. return IGNORE
  100. if print_mode.ignore_audited_here and to_class.name in audited_classes:
  101. return IGNORE
  102. return PRINT
  103. def print_class_dependencies(to_classes: List[class_dependency.JavaClass],
  104. print_mode: PrintMode,
  105. from_class: class_dependency.JavaClass,
  106. direction: str,
  107. audited_classes: Set[str]) -> TargetDependencies:
  108. """Prints the class dependencies to or from a class, grouped by target.
  109. If direction is OUTBOUND and print_mode.ignore_modularized is True, omits
  110. modularized outbound dependencies and returns the build targets that need
  111. to be added for those dependencies. In other cases, returns an empty
  112. TargetDependencies.
  113. If print_mode.ignore_same_package is True, omits outbound dependencies in
  114. the same package.
  115. """
  116. ignore_modularized = direction == OUTBOUND and print_mode.ignore_modularized
  117. bullet_point = '<-' if direction == INBOUND else '->'
  118. print_backlog: List[Tuple[int, str]] = []
  119. # TODO(crbug.com/1124836): This is not quite correct because
  120. # sets considered equal can be converted to different strings. Fix this by
  121. # making JavaClass.build_targets return a List instead of a Set.
  122. suspect_dependencies = 0
  123. target_dependencies = TargetDependencies()
  124. to_classes = sorted(to_classes, key=lambda c: str(c.build_targets))
  125. last_build_target = None
  126. for to_class in to_classes:
  127. # Check if dependency should be ignored due to --ignore-modularized,
  128. # --ignore-same-package, or due to being an ignored class.
  129. # Check if dependency should be listed as a cleared dep.
  130. ignore_allow = categorize_dependency(from_class, to_class,
  131. ignore_modularized, print_mode,
  132. audited_classes)
  133. if ignore_allow == CLEAR:
  134. target_dependencies.update_with_class_node(to_class)
  135. continue
  136. elif ignore_allow == IGNORE:
  137. continue
  138. # Print the dependency
  139. suspect_dependencies += 1
  140. build_target = str(to_class.build_targets)
  141. if last_build_target != build_target:
  142. build_target_names = [
  143. get_build_target_name_to_display(target, print_mode)
  144. for target in to_class.build_targets
  145. ]
  146. build_target_names_string = ", ".join(build_target_names)
  147. print_backlog.append((4, f'[{build_target_names_string}]'))
  148. last_build_target = build_target
  149. display_name = get_class_name_to_display(to_class.name, print_mode)
  150. print_backlog.append((8, f'{bullet_point} {display_name}'))
  151. # Print header
  152. class_name = get_class_name_to_display(from_class.name, print_mode)
  153. if ignore_modularized:
  154. cleared = len(to_classes) - suspect_dependencies
  155. print(f'{class_name} has {suspect_dependencies} outbound dependencies '
  156. f'that may need to be broken (omitted {cleared} cleared '
  157. f'dependencies):')
  158. else:
  159. if direction == INBOUND:
  160. print(f'{class_name} has {len(to_classes)} inbound dependencies:')
  161. else:
  162. print(f'{class_name} has {len(to_classes)} outbound dependencies:')
  163. # Print build targets and dependencies
  164. for indent, message in print_backlog:
  165. indents = ' ' * indent
  166. print(f'{indents}{message}')
  167. return target_dependencies
  168. def print_class_dependencies_for_key(
  169. class_graph: class_dependency.JavaClassDependencyGraph, key: str,
  170. print_mode: PrintMode,
  171. audited_classes: Set[str]) -> TargetDependencies:
  172. """Prints dependencies for a valid key into the class graph."""
  173. target_dependencies = TargetDependencies()
  174. node: class_dependency.JavaClass = class_graph.get_node_by_key(key)
  175. if print_mode.inbound:
  176. print_class_dependencies(graph.sorted_nodes_by_name(node.inbound),
  177. print_mode, node, INBOUND, audited_classes)
  178. if print_mode.outbound:
  179. target_dependencies = print_class_dependencies(
  180. graph.sorted_nodes_by_name(node.outbound), print_mode, node,
  181. OUTBOUND, audited_classes)
  182. return target_dependencies
  183. def main():
  184. """Prints class-level dependencies for one or more input classes."""
  185. arg_parser = argparse.ArgumentParser(
  186. description='Given a JSON dependency graph, output '
  187. 'the class-level dependencies for a given list of classes.')
  188. required_arg_group = arg_parser.add_argument_group('required arguments')
  189. required_arg_group.add_argument(
  190. '-f',
  191. '--file',
  192. required=True,
  193. help='Path to the JSON file containing the dependency graph. '
  194. 'See the README on how to generate this file.')
  195. required_arg_group_either = arg_parser.add_argument_group(
  196. 'required arguments (at least one)')
  197. required_arg_group_either.add_argument(
  198. '-c',
  199. '--classes',
  200. dest='class_names',
  201. help='Case-sensitive name of the classes to print dependencies for. '
  202. 'Matches either the simple class name without package or the fully '
  203. 'qualified class name. For example, `AppHooks` matches '
  204. '`org.chromium.browser.AppHooks`. Specify multiple classes with a '
  205. 'comma-separated list, for example '
  206. '`ChromeActivity,ChromeTabbedActivity`')
  207. required_arg_group_either.add_argument(
  208. '-p',
  209. '--packages',
  210. dest='package_names',
  211. help='Case-sensitive name of the packages to print dependencies for, '
  212. 'such as `org.chromium.browser`. Specify multiple packages with a '
  213. 'comma-separated list.`')
  214. direction_arg_group = arg_parser.add_mutually_exclusive_group()
  215. direction_arg_group.add_argument('--inbound',
  216. dest='inbound_only',
  217. action='store_true',
  218. help='Print inbound dependencies only.')
  219. direction_arg_group.add_argument('--outbound',
  220. dest='outbound_only',
  221. action='store_true',
  222. help='Print outbound dependencies only.')
  223. arg_parser.add_argument('--fully-qualified',
  224. action='store_true',
  225. help='Use fully qualified class names instead of '
  226. 'shortened ones.')
  227. arg_parser.add_argument('--ignore-modularized',
  228. action='store_true',
  229. help='Do not print outbound dependencies on '
  230. 'allowed (modules, components, base, etc.) '
  231. 'dependencies.')
  232. arg_parser.add_argument('--ignore-audited-here',
  233. action='store_true',
  234. help='Do not print outbound dependencies on '
  235. 'other classes being audited in this run.')
  236. arg_parser.add_argument('--ignore-same-package',
  237. action='store_true',
  238. help='Do not print outbound dependencies on '
  239. 'classes in the same package.')
  240. arguments = arg_parser.parse_args()
  241. if not arguments.class_names and not arguments.package_names:
  242. raise ValueError('Either -c/--classes or -p/--packages need to be '
  243. 'specified.')
  244. print_mode = PrintMode(inbound=not arguments.outbound_only,
  245. outbound=not arguments.inbound_only,
  246. ignore_modularized=arguments.ignore_modularized,
  247. ignore_audited_here=arguments.ignore_audited_here,
  248. ignore_same_package=arguments.ignore_same_package,
  249. fully_qualified=arguments.fully_qualified)
  250. class_graph, package_graph, _ = \
  251. serialization.load_class_and_package_graphs_from_file(arguments.file)
  252. valid_class_names = []
  253. if arguments.class_names:
  254. valid_class_names.extend(
  255. print_dependencies_helper.get_valid_classes_from_class_input(
  256. class_graph, arguments.class_names))
  257. if arguments.package_names:
  258. valid_class_names.extend(
  259. print_dependencies_helper.get_valid_classes_from_package_input(
  260. package_graph, arguments.package_names))
  261. target_dependencies = TargetDependencies()
  262. for i, fully_qualified_class_name in enumerate(valid_class_names):
  263. if i > 0:
  264. print()
  265. new_target_deps = print_class_dependencies_for_key(
  266. class_graph, fully_qualified_class_name, print_mode,
  267. set(valid_class_names))
  268. target_dependencies.merge(new_target_deps)
  269. target_dependencies.print()
  270. if __name__ == '__main__':
  271. main()