chromium/chrome/android/features/create_stripped_java_factory.py

#!/usr/bin/env python3
#
# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Generates a stripped down version of a java factory file.

A stripped down factory file is required in a feature's public_java target
during the compilation process so that features can depend on each other
without creating circular dependencies.

Afterwards, the stripped down factory's .class file is excluded from the
resulting target. The real factory uses the feature's internal implementations,
which is why it is not included in the feature's public_java target.

This script generates a stripped down factory file from real factory file to
reduce the burden of maintenance. The stripped down factory will have dummy
implementations of all public methods of the real factory.

This script requires that the real factory file has exactly one top-level class.
"""

import argparse
import datetime
import sys
import os

sys.path.append(
    os.path.join(os.path.dirname(__file__), '..', '..', '..', 'build'))
import action_helpers


# six is a dependency of javalang
sys.path.insert(
    1,
    os.path.join(
        os.path.dirname(__file__), os.pardir, os.pardir, os.pardir,
        'third_party', 'six', 'src'))
sys.path.insert(
    1,
    os.path.join(
        os.path.dirname(__file__), os.pardir, os.pardir, os.pardir,
        'third_party', 'javalang', 'src'))
import javalang

_PARAM_TEMPLATE = '{TYPE} {NAME}'
_METHOD_TEMPLATE = ('{MODIFIERS} {RETURN_TYPE} {NAME} ({PARAMS}) '
                    '{{ return {RETURN_VAL}; }}')
_FILE_TEMPLATE = '''\
// Copyright {YEAR} The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// This file is autogenerated by
//     {SCRIPT_NAME}
// Please do not change its content or use it in actual code ({DNS}).

package {PACKAGE};

{IMPORTS}

{MODIFIERS} class {CLASS_NAME} {{
{METHODS}
}}
'''


def _GetScriptName():
  script_components = os.path.abspath(__file__).split(os.path.sep)
  chrome_index = 0
  for idx, value in enumerate(script_components):
    if value == 'chrome':
      chrome_index = idx
      break
  return os.sep.join(script_components[chrome_index:])


def _GetDefaultReturnVal(type_name):
  if type_name in ('byte', 'short', 'int', 'long', 'float', 'double'):
    return '0'
  elif type_name == 'boolean':
    return 'false'
  elif type_name == 'void':
    return ''
  else:
    return 'null'


def _ParseImports(imports):
  """Returns dict mapping from type name to import path."""
  import_dict = {}
  for import_ in imports:
    if import_.static:
      continue
    assert not import_.wildcard
    name = import_.path.split('.')[-1]
    import_dict[name] = import_.path
  return import_dict


def _ParsePublicMethodsSignatureTypes(clazz):
  """Returns set of type names used in the signatures of all public methods of
  the given class.
  """
  types = set()
  for method in clazz.methods:
    if 'public' in method.modifiers:
      for p in method.parameters:
        types.update(_GetNames(p.type))
      types.update(_GetNames(method.return_type))
  return types


def _GetNames(type_node):
  # Void methods have None as its return_type, taking care of this here makes
  # calling code more readable.
  if type_node is None:
    return []
  if isinstance(type_node, javalang.tree.ReferenceType):
    # TODO: Support sub_type if someone wants to use it.
    names = [type_node.name]
    if type_node.arguments:
      for arg in type_node.arguments:
        names.extend(_GetNames(arg))
    return names
  if isinstance(type_node, javalang.tree.TypeArgument):
    # TODO: Support pattern_type if someone wants to use it.
    return _GetNames(type_node.type)
  if isinstance(type_node, javalang.tree.BasicType):
    # TODO: Support dimensions if someone wants to use it.
    return [type_node.name]
  assert False, 'Unknown type_node={}'.format(type_node)


def _FormatType(type_node):
  if type_node is None:
    return 'void'
  if isinstance(type_node, javalang.tree.ReferenceType):
    # TODO: Support sub_type if someone wants to use it.
    if not type_node.arguments:
      return type_node.name
    formatted_args = (_FormatType(arg) for arg in type_node.arguments)
    return '{name}<{arguments}>'.format(name=type_node.name,
                                        arguments=','.join(formatted_args))
  if isinstance(type_node, javalang.tree.TypeArgument):
    # TODO: Support pattern_type if someone wants to use it.
    return _FormatType(type_node.type)
  if isinstance(type_node, javalang.tree.BasicType):
    # TODO: Support dimensions if someone wants to use it.
    return type_node.name
  assert False, 'Type node {node} cannot be formatted.'.format(node=type_node)


def _FormatMethodModifiers(method_modifiers):
  # Found from:
  # https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.4.3
  modifiers_in_order = ('public', 'protected', 'private', 'abstract', 'static',
                        'final', 'synchronized', 'native', 'strictfp')
  unknown_modifiers = method_modifiers - set(modifiers_in_order)
  assert len(unknown_modifiers) == 0, (
      f'Unknown method modifiers: {unknown_modifiers}')
  return ' '.join([m for m in modifiers_in_order if m in method_modifiers])


def _FormatMethod(method):
  params = []
  for param in method.parameters:
    param_dict = {
        'TYPE': _FormatType(param.type),
        'NAME': param.name,
    }
    params.append(_PARAM_TEMPLATE.format(**param_dict))
  return_type = _FormatType(method.return_type)
  method_dict = {
      'MODIFIERS': _FormatMethodModifiers(method.modifiers),
      'RETURN_TYPE': return_type,
      'NAME': method.name,
      'PARAMS': ', '.join(params),
      'RETURN_VAL': _GetDefaultReturnVal(return_type),
  }
  return (_METHOD_TEMPLATE.format(**method_dict))


def _FormatPublicMethods(clazz):
  methods = []
  for method in clazz.methods:
    if 'public' in method.modifiers:
      methods.append(_FormatMethod(method))
  return methods


def _FilterAndFormatImports(import_dict, signature_types):
  """Returns formatted imports required by the passed signature types."""
  formatted_imports = [
      'import %s;' % import_dict[t] for t in signature_types if t in import_dict
  ]
  return sorted(formatted_imports)


def main(args):
  parser = argparse.ArgumentParser()
  parser.add_argument('--input', required=True, help='Input java file path.')
  parser.add_argument('--output', required=True, help='Output java file path.')
  options = parser.parse_args(args)

  with open(options.input, 'r') as f:
    content = f.read()

  java_ast = javalang.parse.parse(content)
  assert len(java_ast.types) == 1, 'Can only process Java files with one class'
  clazz = java_ast.types[0]
  import_dict = _ParseImports(java_ast.imports)
  signature_types = _ParsePublicMethodsSignatureTypes(clazz)
  formatted_public_methods = _FormatPublicMethods(clazz)
  formatted_imports = _FilterAndFormatImports(import_dict, signature_types)

  file_dict = {
      # This is necessary for this file to not trigger presubmit errors.
      'DNS': ' '.join(['DO', 'NOT', 'SUBMIT']),
      'YEAR': str(datetime.date.today().year),
      'SCRIPT_NAME': _GetScriptName(),
      'PACKAGE': java_ast.package.name,
      'IMPORTS': '\n'.join(formatted_imports),
      'MODIFIERS': ' '.join(clazz.modifiers),
      'CLASS_NAME': clazz.name,
      'METHODS': '\n'.join(['    ' + m for m in formatted_public_methods])
  }
  with action_helpers.atomic_output(options.output, mode='w') as f:
    f.write(_FILE_TEMPLATE.format(**file_dict))


if __name__ == '__main__':
  main(sys.argv[1:])