#!/usr/bin/env python
#
# Copyright (C) 2018 Google, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import re
# Matches a JavaDoc comment followed by an @Rpc annotation.
import subprocess
"""A regex that captures the JavaDoc comment and function signature."""
JAVADOC_RPC_REGEX = re.compile(
# Capture the entire comment string.
r'(?P<comment>/\*\*(?:(?!/\*\*).)*?\*/)(?:(?:(?!\*/).)*?)\s*'
# Find at least one @Rpc Annotation
r'(?:@\w+\s*)*?(?:@Rpc.*?\s*)(?:@\w+\s*)*?'
# Capture the function signature, ignoring the throws statement
# (the throws information will be pulled from the comment).
r'(?P<function_signature>.*?)(?:throws.*?)?{',
flags=re.MULTILINE | re.DOTALL)
"""
Captures javadoc "frills" like the ones found below:
/**
*
*/
/** */
"""
CAPTURE_JAVADOC_FRILLS = re.compile(
r'(^\s*(/\*\*$|/\*\* |\*($| ))|\s*\*/\s*$)',
re.MULTILINE)
"""A regex to capture the individual pieces of the function signature."""
CAPTURE_FUNCTION_SIGNATURE = re.compile(
# Capture any non-static function
r'(public|private)'
# Allow synchronized and @Annotations()
r'[( synchronized)(@\w+\(.*?\)?)]*?'
# Return Type (Allow n-number of generics and arrays)
r'(?P<return_type>\w+(?:[\[\]<>\w ,]*?)?)\s+'
# Capture functionName
r'(?P<function_name>\w*)\s*'
# Capture anything enclosed in parens
r'\((?P<parameters>.*)\)',
re.MULTILINE | re.DOTALL)
"""Matches a parameter and its RPC annotations."""
CAPTURE_PARAMETER = re.compile(
r'(?:'
r'(?P<optional>@RpcOptional\s+)?'
r'(?P<rpc_param>@RpcParameter\(.*?\)\s*)?'
r'(?P<default>@RpcDefault\((?P<default_value>.*)\)\s*)?'
r')*'
r'(?P<param_type>\w+)\s+(?P<param_name>\w+)',
flags=re.MULTILINE | re.DOTALL)
class Facade(object):
"""A class representing a Facade.
Attributes:
path: the path the facade is located at.
directory: the
"""
def __init__(self, path):
self.path = path
self.directory = os.path.dirname(self.path)
# -5 removes the '.java' file extension
self.name = path[path.rfind('/') + 1:-5]
self.rpcs = list()
def main():
basepath = os.path.abspath(os.path.join(os.path.dirname(
os.path.realpath(__file__)), '..'))
facades = list()
for path, dirs, files in os.walk(basepath):
for file_name in files:
if file_name.endswith('Facade.java'):
facades.append(parse_facade_file(os.path.join(path, file_name)))
basepath = os.path.abspath(os.path.join(os.path.dirname(
os.path.realpath(__file__)), '..'))
write_output(facades, os.path.join(basepath, 'Docs/ApiReference.md'))
def write_output(facades, output_path):
facades = sorted(facades, key=lambda x: x.directory)
git_rev = None
try:
git_rev = subprocess.check_output('git rev-parse HEAD',
shell=True).decode('utf-8').strip()
except subprocess.CalledProcessError as e:
# Getting the commit ID is optional; we continue if we cannot get it
pass
with open(output_path, 'w') as fd:
if git_rev:
fd.write('Generated at commit `%s`\n\n' % git_rev)
fd.write('# Facade Groups')
prev_directory = ''
for facade in facades:
if facade.directory != prev_directory:
fd.write('\n\n## %s\n\n' % facade.directory[
facade.directory.rfind('/') + 1:])
prev_directory = facade.directory
fd.write(' * [%s](#%s)\n' % (facade.name, facade.name.lower()))
fd.write('\n# Facades\n\n')
for facade in facades:
fd.write('\n## %s' % facade.name)
for rpc in facade.rpcs:
fd.write('\n\n### %s\n\n' % rpc.name)
fd.write('%s\n' % rpc)
def parse_facade_file(file_path):
"""Parses a .*Facade.java file and represents it as a Facade object"""
facade = Facade(file_path)
with open(file_path, 'r') as content_file:
content = content_file.read()
matches = re.findall(JAVADOC_RPC_REGEX, content)
for match in matches:
rpc_function = DocumentedFunction(
match[0].replace('\\n', '\n'), # match[0]: JavaDoc comment
match[1].replace('\\n', '\n')) # match[1]: function signature
facade.rpcs.append(rpc_function)
facade.rpcs.sort(key=lambda rpc: rpc.name)
return facade
class DefaultValue(object):
"""An object representation of a default value.
Functions as Optional in Java, or a pointer in C++.
Attributes:
value: the default value
"""
def __init__(self, default_value=None):
self.value = default_value
class DocumentedValue(object):
def __init__(self):
"""Creates an empty DocumentedValue object."""
self.type = 'void'
self.name = None
self.description = None
self.default_value = None
def set_type(self, param_type):
self.type = param_type
return self
def set_name(self, name):
self.name = name
return self
def set_description(self, description):
self.description = description
return self
def set_default_value(self, default_value):
self.default_value = default_value
return self
def __str__(self):
if self.name is None:
return self.description
if self.default_value is None:
return '%s: %s' % (self.name, self.description)
else:
return '%s: %s (default: %s)' % (self.name, self.description,
self.default_value.value)
class DocumentedFunction(object):
"""A combination of all function documentation into a single object.
Attributes:
_description: A string that describes the function.
_parameters: A dictionary of {parameter name: DocumentedValue object}
_return: a DocumentedValue with information on the returned value.
_throws: A dictionary of {throw type (str): DocumentedValue object}
"""
def __init__(self, comment, function_signature):
self._name = None
self._description = None
self._parameters = {}
self._return = DocumentedValue()
self._throws = {}
self._parse_comment(comment)
self._parse_function_signature(function_signature)
@property
def name(self):
return self._name
def _parse_comment(self, comment):
"""Parses a JavaDoc comment into DocumentedFunction attributes."""
comment = str(re.sub(CAPTURE_JAVADOC_FRILLS, '', comment))
tag = 'description'
tag_data = ''
for line in comment.split('\n'):
line.strip()
if line.startswith('@'):
self._finalize_tag(tag, tag_data)
tag_end_index = line.find(' ')
tag = line[1:tag_end_index]
tag_data = line[tag_end_index + 1:]
else:
if not tag_data:
whitespace_char = ''
elif (line.startswith(' ')
or tag_data.endswith('\n')
or line == ''):
whitespace_char = '\n'
else:
whitespace_char = ' '
tag_data = '%s%s%s' % (tag_data, whitespace_char, line)
self._finalize_tag(tag, tag_data.strip())
def __str__(self):
params_signature = ', '.join(['%s %s' % (param.type, param.name)
for param in self._parameters.values()])
params_description = '\n '.join(['%s: %s' % (param.name,
param.description)
for param in
self._parameters.values()])
if params_description:
params_description = ('\n**Parameters:**\n\n %s\n' %
params_description)
return_description = '\n' if self._return else ''
if self._return:
return_description += ('**Returns:**\n\n %s' %
self._return.description)
return (
# ReturnType functionName(Parameters)
'%s %s(%s)\n\n'
# Description
'%s\n'
# Params & Return
'%s%s' % (self._return.type, self._name,
params_signature, self._description,
params_description, return_description)).strip()
def _parse_function_signature(self, function_signature):
"""Parses the function signature into DocumentedFunction attributes."""
header_match = re.search(CAPTURE_FUNCTION_SIGNATURE, function_signature)
self._name = header_match.group('function_name')
self._return.set_type(header_match.group('return_type'))
for match in re.finditer(CAPTURE_PARAMETER,
header_match.group('parameters')):
param_name = match.group('param_name')
param_type = match.group('param_type')
if match.group('default_value'):
default = DefaultValue(match.group('default_value'))
elif match.group('optional'):
default = DefaultValue(None)
else:
default = None
if param_name in self._parameters:
param = self._parameters[param_name]
else:
param = DocumentedValue()
param.set_type(param_type)
param.set_name(param_name)
param.set_default_value(default)
def _finalize_tag(self, tag, tag_data):
"""Finalize the JavaDoc @tag by adding it to the correct field."""
name = tag_data[:tag_data.find(' ')]
description = tag_data[tag_data.find(' ') + 1:].strip()
if tag == 'description':
self._description = tag_data
elif tag == 'param':
if name in self._parameters:
param = self._parameters[name]
else:
param = DocumentedValue().set_name(name)
self._parameters[name] = param
param.set_description(description)
elif tag == 'return':
self._return.set_description(tag_data)
elif tag == 'throws':
new_throws = DocumentedValue().set_name(name)
new_throws.set_description(description)
self._throws[name] = new_throws
if __name__ == '__main__':
main()