| #!/usr/bin/env python3 |
| # Copyright 2020 The Chromium Authors |
| # 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/40147556): 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() |