blob: 69be0a3a089bba8c4680277645c63156bb8db438 [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2019 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.
"""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 stripped down factory files from real factory files to
reduce the burden of maintenance. It checks to make sure that these factory
files are as simple as possible and replaces multi-line java return statements
with 'return null;'.
In order to keep the regex simple and easily understood, the java factory files
have a few strict requirements. This avoids the need to take care of every
corner case involving comments or strings. Since the generated factory file is
only used during the build, and the real one is used at runtime, it is
sufficient to have the build pass with simple modifications.
We require that these factory files to:
- Contain exactly one top-level class.
- No inner classes.
- Contain a private constructor with no arguments (it won't be used).
- Contain only public static methods apart from the constructor.
- Only void and object return types are supported (no primitives).
- Avoid explicitly importing internal classes and their dependencies.
- The factory should be in the same package as the internal classes it
instantiates.
"""
import argparse
import re
import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__),
os.pardir, os.pardir, os.pardir,
'build', 'android', 'gyp'))
from util import build_utils
# Matches multi-line public static methods:
# '^( public static ' - Matches the first part of the method signature.
# '(?!void)' - Do not match methods signatures that return void.
# '[^{]+ {)' - Matches the rest of the method signature until the opening brace.
# '.+?' - Non-greedy match to only match one method body.
# '^( })$' - Matches the closing brace of the method.
_RE_PUBLIC_STATIC_NOT_VOID_METHOD = re.compile(
r'^( public static (?!void)[^{]+ {).+?^( })$',
re.DOTALL | re.MULTILINE)
# Replacement string to replace the entire method body with a null return.
_RETURN_NULL = r'''\g<1>
return null;
\g<2>'''
# Matches multi-line public static void methods:
# Same as for public static methods, explicitly specify void return type.
_RE_PUBLIC_STATIC_VOID_METHOD = re.compile(
r'^( public static void [^{]+ {).+?^( })$', re.DOTALL | re.MULTILINE)
# Replacement string to replace the entire method body with an empty return.
_RETURN = r'''\g<1>
return;
\g<2>'''
_RE_CLASS = re.compile(r'\bclass (\w+)\b')
_PRIVATE_CONSTRUCTOR = 'private {0}() {{}}\n'
def _ParseArgs(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)
return options
def _EnsureSimpleFactory(content):
"""Assert that the java factory file is very simple.
This is important since we are using the build system to swap out .class
files of corresponding generated and internal java factory files. Keeping the
factory files as simple as possible minimizes the overhead of reasoning about
the factory code. It is already sufficiently complex to deduce which file a
target is actually using when calling one of these factories.
"""
classes = _RE_CLASS.findall(content)
assert len(classes) == 1, 'Factory must contain exactly one class.'
class_name = classes[0]
assert _PRIVATE_CONSTRUCTOR.format(class_name) in content, (
'Factory must have a private constructor with no arguments.')
def _ReplaceMethodBodies(content):
content = _RE_PUBLIC_STATIC_NOT_VOID_METHOD.sub(_RETURN_NULL, content)
content = _RE_PUBLIC_STATIC_VOID_METHOD.sub(_RETURN, content)
return content
def main(args):
options = _ParseArgs(args)
with open(options.input, 'r') as f:
content = f.read()
_EnsureSimpleFactory(content)
replaced_content = _ReplaceMethodBodies(content)
with build_utils.AtomicOutput(options.output) as f:
f.write(replaced_content)
if __name__ == '__main__':
main(sys.argv[1:])