123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- #!/usr/bin/env python3
- # Copyright 2020 The Chromium Authors. All rights reserved.
- # Use of this source code is governed by a BSD-style license that can be
- # found in the LICENSE file.
- """Command-line tool for printing class-level dependencies."""
- import argparse
- from dataclasses import dataclass
- from typing import List, Set, Tuple
- import chrome_names
- import class_dependency
- import graph
- import package_dependency
- import print_dependencies_helper
- import serialization
- # Return values of categorize_dependency().
- IGNORE = 'ignore'
- CLEAR = 'clear'
- PRINT = 'print'
- @dataclass
- class PrintMode:
- """Options of how and which dependencies to output."""
- inbound: bool
- outbound: bool
- ignore_modularized: bool
- ignore_audited_here: bool
- ignore_same_package: bool
- fully_qualified: bool
- class TargetDependencies:
- """Build target dependencies that the set of classes depends on."""
- def __init__(self):
- # Build targets usable by modularized code that need to be depended on.
- self.cleared: Set[str] = set()
- # Build targets usable by modularized code that might need to be
- # depended on. This happens rarely, when a class dependency is in
- # multiple build targets (Android resource .R classes, or due to
- # bytecode rewriting).
- self.dubious: Set[str] = set()
- def update_with_class_node(self, class_node: class_dependency.JavaClass):
- if len(class_node.build_targets) == 1:
- self.cleared.update(class_node.build_targets)
- else:
- self.dubious.update(class_node.build_targets)
- def merge(self, other: 'TargetDependencies'):
- self.cleared.update(other.cleared)
- self.dubious.update(other.dubious)
- def print(self):
- if self.cleared:
- print()
- print('Cleared dependencies:')
- for dep in sorted(self.cleared):
- print(dep)
- if self.dubious:
- print()
- print('Dubious dependencies due to classes with multiple build '
- 'targets. Only some of these are required:')
- for dep in sorted(self.dubious):
- print(dep)
- INBOUND = 'inbound'
- OUTBOUND = 'outbound'
- ALLOWED_PREFIXES = {
- '//base/',
- '//base:',
- '//chrome/browser/',
- '//components/',
- '//content/',
- '//ui/',
- '//url:',
- }
- IGNORED_CLASSES = {'org.chromium.base.natives.GEN_JNI'}
- def get_class_name_to_display(fully_qualified_name: str,
- print_mode: PrintMode) -> str:
- if print_mode.fully_qualified:
- return fully_qualified_name
- else:
- return chrome_names.shorten_class(fully_qualified_name)
- def get_build_target_name_to_display(build_target: str,
- print_mode: PrintMode) -> str:
- if print_mode.fully_qualified:
- return build_target
- else:
- return chrome_names.shorten_build_target(build_target)
- def is_allowed_target_dependency(build_target: str) -> bool:
- return any(build_target.startswith(p) for p in ALLOWED_PREFIXES)
- def is_ignored_class_dependency(class_name: str) -> bool:
- return class_name in IGNORED_CLASSES
- def categorize_dependency(from_class: class_dependency.JavaClass,
- to_class: class_dependency.JavaClass,
- ignore_modularized: bool, print_mode: PrintMode,
- audited_classes: Set[str]) -> str:
- """Decides if a class dependency should be printed, cleared, or ignored."""
- if is_ignored_class_dependency(to_class.name):
- return IGNORE
- if ignore_modularized and all(
- is_allowed_target_dependency(target)
- for target in to_class.build_targets):
- return CLEAR
- if (print_mode.ignore_same_package
- and to_class.package == from_class.package):
- return IGNORE
- if print_mode.ignore_audited_here and to_class.name in audited_classes:
- return IGNORE
- return PRINT
- def print_class_dependencies(to_classes: List[class_dependency.JavaClass],
- print_mode: PrintMode,
- from_class: class_dependency.JavaClass,
- direction: str,
- audited_classes: Set[str]) -> TargetDependencies:
- """Prints the class dependencies to or from a class, grouped by target.
- If direction is OUTBOUND and print_mode.ignore_modularized is True, omits
- modularized outbound dependencies and returns the build targets that need
- to be added for those dependencies. In other cases, returns an empty
- TargetDependencies.
- If print_mode.ignore_same_package is True, omits outbound dependencies in
- the same package.
- """
- ignore_modularized = direction == OUTBOUND and print_mode.ignore_modularized
- bullet_point = '<-' if direction == INBOUND else '->'
- print_backlog: List[Tuple[int, str]] = []
- # TODO(crbug.com/1124836): This is not quite correct because
- # sets considered equal can be converted to different strings. Fix this by
- # making JavaClass.build_targets return a List instead of a Set.
- suspect_dependencies = 0
- target_dependencies = TargetDependencies()
- to_classes = sorted(to_classes, key=lambda c: str(c.build_targets))
- last_build_target = None
- for to_class in to_classes:
- # Check if dependency should be ignored due to --ignore-modularized,
- # --ignore-same-package, or due to being an ignored class.
- # Check if dependency should be listed as a cleared dep.
- ignore_allow = categorize_dependency(from_class, to_class,
- ignore_modularized, print_mode,
- audited_classes)
- if ignore_allow == CLEAR:
- target_dependencies.update_with_class_node(to_class)
- continue
- elif ignore_allow == IGNORE:
- continue
- # Print the dependency
- suspect_dependencies += 1
- build_target = str(to_class.build_targets)
- if last_build_target != build_target:
- build_target_names = [
- get_build_target_name_to_display(target, print_mode)
- for target in to_class.build_targets
- ]
- build_target_names_string = ", ".join(build_target_names)
- print_backlog.append((4, f'[{build_target_names_string}]'))
- last_build_target = build_target
- display_name = get_class_name_to_display(to_class.name, print_mode)
- print_backlog.append((8, f'{bullet_point} {display_name}'))
- # Print header
- class_name = get_class_name_to_display(from_class.name, print_mode)
- if ignore_modularized:
- cleared = len(to_classes) - suspect_dependencies
- print(f'{class_name} has {suspect_dependencies} outbound dependencies '
- f'that may need to be broken (omitted {cleared} cleared '
- f'dependencies):')
- else:
- if direction == INBOUND:
- print(f'{class_name} has {len(to_classes)} inbound dependencies:')
- else:
- print(f'{class_name} has {len(to_classes)} outbound dependencies:')
- # Print build targets and dependencies
- for indent, message in print_backlog:
- indents = ' ' * indent
- print(f'{indents}{message}')
- return target_dependencies
- def print_class_dependencies_for_key(
- class_graph: class_dependency.JavaClassDependencyGraph, key: str,
- print_mode: PrintMode,
- audited_classes: Set[str]) -> TargetDependencies:
- """Prints dependencies for a valid key into the class graph."""
- target_dependencies = TargetDependencies()
- node: class_dependency.JavaClass = class_graph.get_node_by_key(key)
- if print_mode.inbound:
- print_class_dependencies(graph.sorted_nodes_by_name(node.inbound),
- print_mode, node, INBOUND, audited_classes)
- if print_mode.outbound:
- target_dependencies = print_class_dependencies(
- graph.sorted_nodes_by_name(node.outbound), print_mode, node,
- OUTBOUND, audited_classes)
- return target_dependencies
- def main():
- """Prints class-level dependencies for one or more input classes."""
- arg_parser = argparse.ArgumentParser(
- description='Given a JSON dependency graph, output '
- 'the class-level dependencies for a given list of classes.')
- required_arg_group = arg_parser.add_argument_group('required arguments')
- required_arg_group.add_argument(
- '-f',
- '--file',
- required=True,
- help='Path to the JSON file containing the dependency graph. '
- 'See the README on how to generate this file.')
- required_arg_group_either = arg_parser.add_argument_group(
- 'required arguments (at least one)')
- required_arg_group_either.add_argument(
- '-c',
- '--classes',
- dest='class_names',
- help='Case-sensitive name of the classes to print dependencies for. '
- 'Matches either the simple class name without package or the fully '
- 'qualified class name. For example, `AppHooks` matches '
- '`org.chromium.browser.AppHooks`. Specify multiple classes with a '
- 'comma-separated list, for example '
- '`ChromeActivity,ChromeTabbedActivity`')
- required_arg_group_either.add_argument(
- '-p',
- '--packages',
- dest='package_names',
- help='Case-sensitive name of the packages to print dependencies for, '
- 'such as `org.chromium.browser`. Specify multiple packages with a '
- 'comma-separated list.`')
- direction_arg_group = arg_parser.add_mutually_exclusive_group()
- direction_arg_group.add_argument('--inbound',
- dest='inbound_only',
- action='store_true',
- help='Print inbound dependencies only.')
- direction_arg_group.add_argument('--outbound',
- dest='outbound_only',
- action='store_true',
- help='Print outbound dependencies only.')
- arg_parser.add_argument('--fully-qualified',
- action='store_true',
- help='Use fully qualified class names instead of '
- 'shortened ones.')
- arg_parser.add_argument('--ignore-modularized',
- action='store_true',
- help='Do not print outbound dependencies on '
- 'allowed (modules, components, base, etc.) '
- 'dependencies.')
- arg_parser.add_argument('--ignore-audited-here',
- action='store_true',
- help='Do not print outbound dependencies on '
- 'other classes being audited in this run.')
- arg_parser.add_argument('--ignore-same-package',
- action='store_true',
- help='Do not print outbound dependencies on '
- 'classes in the same package.')
- arguments = arg_parser.parse_args()
- if not arguments.class_names and not arguments.package_names:
- raise ValueError('Either -c/--classes or -p/--packages need to be '
- 'specified.')
- print_mode = PrintMode(inbound=not arguments.outbound_only,
- outbound=not arguments.inbound_only,
- ignore_modularized=arguments.ignore_modularized,
- ignore_audited_here=arguments.ignore_audited_here,
- ignore_same_package=arguments.ignore_same_package,
- fully_qualified=arguments.fully_qualified)
- class_graph, package_graph, _ = \
- serialization.load_class_and_package_graphs_from_file(arguments.file)
- valid_class_names = []
- if arguments.class_names:
- valid_class_names.extend(
- print_dependencies_helper.get_valid_classes_from_class_input(
- class_graph, arguments.class_names))
- if arguments.package_names:
- valid_class_names.extend(
- print_dependencies_helper.get_valid_classes_from_package_input(
- package_graph, arguments.package_names))
- target_dependencies = TargetDependencies()
- for i, fully_qualified_class_name in enumerate(valid_class_names):
- if i > 0:
- print()
- new_target_deps = print_class_dependencies_for_key(
- class_graph, fully_qualified_class_name, print_mode,
- set(valid_class_names))
- target_dependencies.merge(new_target_deps)
- target_dependencies.print()
- if __name__ == '__main__':
- main()
|