| # Copyright 2023 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| import dataclasses |
| import os |
| import re |
| from typing import List |
| from typing import Optional |
| |
| import models |
| import type_resolver |
| |
| |
| @dataclasses.dataclass |
| class ParsedMethod: |
| name: str |
| return_type: str |
| params: List[models.Param] |
| native_class_name: str |
| |
| |
| @dataclasses.dataclass |
| class ParsedFile: |
| filename: str |
| java_class: models.JavaClass |
| type_resolver: type_resolver.TypeResolver |
| proxy_methods: List[ParsedMethod] |
| non_proxy_natives: list # [jni_generator.NativeMethod] |
| called_by_natives: list # [jni_generator.CalledByNative] |
| proxy_interface: Optional[models.JavaClass] = None |
| module_name: Optional[str] = None # E.g. @NativeMethods("module_name") |
| jni_namespace: Optional[str] = None # E.g. @JNINamespace("content") |
| |
| |
| @dataclasses.dataclass |
| class _ParsedProxyNatives: |
| interface_name: str |
| visibility: str |
| module_name: str |
| methods: List[ParsedMethod] |
| |
| |
| # Match single line comments, multiline comments, character literals, and |
| # double-quoted strings. |
| _COMMENT_REMOVER_REGEX = re.compile( |
| r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', |
| re.DOTALL | re.MULTILINE) |
| |
| |
| def _remove_comments(contents): |
| # We need to support both inline and block comments, and we need to handle |
| # strings that contain '//' or '/*'. |
| def replacer(match): |
| # Replace matches that are comments with nothing; return literals/strings |
| # unchanged. |
| s = match.group(0) |
| if s.startswith('/'): |
| return '' |
| else: |
| return s |
| |
| return _COMMENT_REMOVER_REGEX.sub(replacer, contents) |
| |
| |
| _PACKAGE_REGEX = re.compile('^package\s+(\S+?);', flags=re.MULTILINE) |
| |
| |
| def _parse_package(contents): |
| match = _PACKAGE_REGEX.search(contents) |
| if not match: |
| raise SyntaxError('Unable to find "package" line') |
| return match.group(1) |
| |
| |
| _CLASSES_REGEX = re.compile( |
| r'^(.*?)(?:\b(public|protected|private)?\b)\s*' |
| r'(?:\b(?:static|abstract|final|sealed)\s+)*' |
| r'\b(?:class|interface|enum)\s+(\w+?)\b[^"]*?$', |
| flags=re.MULTILINE) |
| |
| |
| # Does not handle doubly-nested classes. |
| def _parse_java_classes(contents): |
| package = _parse_package(contents).replace('.', '/') |
| outer_class = None |
| nested_classes = [] |
| for m in _CLASSES_REGEX.finditer(contents): |
| preamble, visibility, class_name = m.groups() |
| # Ignore annoations like @Foo("contains the words class Bar") |
| if preamble.count('"') % 2 != 0: |
| continue |
| if outer_class is None: |
| outer_class = models.JavaClass(f'{package}/{class_name}', visibility) |
| else: |
| nested_classes.append(outer_class.make_nested(class_name, visibility)) |
| |
| if outer_class is None: |
| raise SyntaxError('No classes found.') |
| |
| return outer_class, nested_classes |
| |
| |
| def parse_javap_signature(signature_line): |
| prefix = 'Signature: ' |
| index = signature_line.find(prefix) |
| if index == -1: |
| prefix = 'descriptor: ' |
| index = signature_line.index(prefix) |
| return '"%s"' % signature_line[index + len(prefix):] |
| |
| |
| def strip_generics(value): |
| """Strips Java generics from a string.""" |
| nest_level = 0 # How deeply we are nested inside the generics. |
| start_index = 0 # Starting index of the last non-generic region. |
| out = [] |
| |
| for i, c in enumerate(value): |
| if c == '<': |
| if nest_level == 0: |
| out.append(value[start_index:i]) |
| nest_level += 1 |
| elif c == '>': |
| start_index = i + 1 |
| nest_level -= 1 |
| out.append(value[start_index:]) |
| return ''.join(out) |
| |
| |
| # Supports only @Foo and @Foo("value"). |
| _ANNOTATION_REGEX = re.compile(r'@([\w.]+)(?:\(\s*"(.*?)\"\s*\))?\s*') |
| |
| |
| def _parse_type_with_annotations(value): |
| annotations = {} |
| last_idx = 0 |
| for m in _ANNOTATION_REGEX.finditer(value): |
| annotations[m.group(1)] = m.group(2) |
| last_idx = m.end() |
| |
| return annotations, value[last_idx:] |
| |
| |
| _FINAL_REGEX = re.compile(r'\bfinal\s') |
| |
| |
| def parse_param_list(line, from_javap=False): |
| """Parses the params into a list of Param objects.""" |
| if not line: |
| return [] |
| ret = [] |
| line = strip_generics(line) |
| line = _FINAL_REGEX.sub('', line) |
| for p in line.split(','): |
| annotations, p = _parse_type_with_annotations(p) |
| |
| items = p.split() |
| datatype = items[0] |
| # Handle varargs. |
| if datatype.endswith('...'): |
| datatype = datatype[:-3] + '[]' |
| |
| if from_javap: |
| datatype = datatype.replace('.', '/') |
| |
| name = items[1] if len(items) > 1 else 'p%s' % len(ret) |
| |
| ret.append( |
| models.Param(annotations=list(annotations), |
| datatype=datatype, |
| name=name)) |
| |
| return ret |
| |
| |
| _NATIVE_PROXY_EXTRACTION_REGEX = re.compile( |
| r'@NativeMethods(?:\(\s*"(?P<module_name>\w+)"\s*\))?[\S\s]+?' |
| r'(?P<visibility>public)?\s*\binterface\s*' |
| r'(?P<interface_name>\w*)\s*{(?P<interface_body>(\s*.*)+?\s*)}') |
| |
| # Matches on method declarations unlike _EXTRACT_NATIVES_REGEX |
| # doesn't require name to be prefixed with native, and does not |
| # require a native qualifier. |
| _EXTRACT_METHODS_REGEX = re.compile(r'\s*(.*?)\s+(\w+)\((.*?)\);', |
| flags=re.DOTALL) |
| |
| _PUBLIC_REGEX = re.compile(r'\bpublic\s') |
| |
| |
| def _parse_proxy_natives(contents, resolver): |
| matches = list(_NATIVE_PROXY_EXTRACTION_REGEX.finditer(contents)) |
| if not matches: |
| return None |
| if len(matches) > 1: |
| raise SyntaxError( |
| 'Multiple @NativeMethod interfaces in one class is not supported.') |
| |
| match = matches[0] |
| ret = _ParsedProxyNatives(interface_name=match.group('interface_name'), |
| visibility=match.group('visibility'), |
| module_name=match.group('module_name'), |
| methods=[]) |
| interface_body = match.group('interface_body') |
| |
| for m in _EXTRACT_METHODS_REGEX.finditer(interface_body): |
| preamble, name, params_part = m.groups() |
| preamble = _PUBLIC_REGEX.sub('', preamble) |
| annotations, return_type_part = _parse_type_with_annotations(preamble) |
| params = parse_param_list(params_part) |
| # Ensure all nested class types have "OuterClass." prefixed on them. |
| params = [ |
| models.Param(annotations=p.annotations, |
| datatype=resolver.resolve_type(p.datatype), |
| name=p.name) for p in params |
| ] |
| return_type = resolver.resolve_type(strip_generics(return_type_part)) |
| ret.methods.append( |
| ParsedMethod( |
| name=name, |
| return_type=return_type, |
| params=params, |
| native_class_name=annotations.get('NativeClassQualifiedName'))) |
| if not ret.methods: |
| raise SyntaxError('Found no methods within @NativeMethod interface.') |
| return ret |
| |
| |
| _IMPORT_REGEX = re.compile(r'^import\s+([^\s*]+);', flags=re.MULTILINE) |
| _IMPORT_CLASS_NAME_REGEX = re.compile(r'^(.*?)\.([A-Z].*)') |
| |
| |
| def _parse_imports(contents): |
| # Regex skips static imports as well as wildcard imports. |
| names = _IMPORT_REGEX.findall(contents) |
| for name in names: |
| m = _IMPORT_CLASS_NAME_REGEX.match(name) |
| if m: |
| package, class_name = m.groups() |
| yield models.JavaClass( |
| package.replace('.', '/') + '/' + class_name.replace('.', '$')) |
| |
| |
| _JNI_NAMESPACE_REGEX = re.compile('@JNINamespace\("(.*?)"\)') |
| |
| |
| def _parse_jni_namespace(contents): |
| m = _JNI_NAMESPACE_REGEX.findall(contents) |
| if not m: |
| return '' |
| if len(m) > 1: |
| raise SyntaxError('Found multiple @JNINamespace attributes.') |
| return m[0] |
| |
| |
| def _do_parse(filename, *, package_prefix): |
| assert not filename.endswith('.kt'), ( |
| f'Found {filename}, but Kotlin is not supported by JNI generator.') |
| with open(filename) as f: |
| contents = f.read() |
| contents = _remove_comments(contents) |
| |
| outer_class, nested_classes = _parse_java_classes(contents) |
| |
| expected_name = os.path.splitext(os.path.basename(filename))[0] |
| if outer_class.name != expected_name: |
| raise SyntaxError( |
| f'Found class "{outer_class.name}" but expected "{expected_name}".') |
| |
| if package_prefix: |
| outer_class = outer_class.make_prefixed(package_prefix) |
| nested_classes = [c.make_prefixed(package_prefix) for c in nested_classes] |
| |
| resolver = type_resolver.TypeResolver(outer_class) |
| for java_class in _parse_imports(contents): |
| resolver.add_import(java_class) |
| for java_class in nested_classes: |
| resolver.add_nested_class(java_class) |
| |
| parsed_proxy_natives = _parse_proxy_natives(contents, resolver) |
| jni_namespace = _parse_jni_namespace(contents) |
| |
| # TODO(crbug.com/1406605): Remove circular dep. |
| import jni_generator |
| non_proxy_natives = jni_generator.ExtractNatives(contents, 'long') |
| called_by_natives = jni_generator.ExtractCalledByNatives(resolver, contents) |
| |
| ret = ParsedFile(filename=filename, |
| jni_namespace=jni_namespace, |
| java_class=outer_class, |
| type_resolver=resolver, |
| proxy_methods=[], |
| non_proxy_natives=non_proxy_natives, |
| called_by_natives=called_by_natives) |
| |
| if parsed_proxy_natives: |
| ret.module_name = parsed_proxy_natives.module_name |
| ret.proxy_interface = outer_class.make_nested( |
| parsed_proxy_natives.interface_name, |
| visibility=parsed_proxy_natives.visibility) |
| ret.proxy_methods = parsed_proxy_natives.methods |
| |
| return ret |
| |
| |
| def parse_java_file(filename, *, package_prefix=None): |
| try: |
| return _do_parse(filename, package_prefix=package_prefix) |
| except SyntaxError as e: |
| e.msg += f' (when parsing {filename})' |
| raise |