1*456ef56aSSadaf Ebrahimi#!/usr/bin/env python 2*456ef56aSSadaf Ebrahimi# 3*456ef56aSSadaf Ebrahimi# Copyright (C) 2018 Google, Inc. 4*456ef56aSSadaf Ebrahimi# 5*456ef56aSSadaf Ebrahimi# Licensed under the Apache License, Version 2.0 (the "License"); 6*456ef56aSSadaf Ebrahimi# you may not use this file except in compliance with the License. 7*456ef56aSSadaf Ebrahimi# You may obtain a copy of the License at: 8*456ef56aSSadaf Ebrahimi# 9*456ef56aSSadaf Ebrahimi# http://www.apache.org/licenses/LICENSE-2.0 10*456ef56aSSadaf Ebrahimi# 11*456ef56aSSadaf Ebrahimi# Unless required by applicable law or agreed to in writing, software 12*456ef56aSSadaf Ebrahimi# distributed under the License is distributed on an "AS IS" BASIS, 13*456ef56aSSadaf Ebrahimi# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14*456ef56aSSadaf Ebrahimi# See the License for the specific language governing permissions and 15*456ef56aSSadaf Ebrahimi# limitations under the License. 16*456ef56aSSadaf Ebrahimiimport os 17*456ef56aSSadaf Ebrahimiimport re 18*456ef56aSSadaf Ebrahimi 19*456ef56aSSadaf Ebrahimi# Matches a JavaDoc comment followed by an @Rpc annotation. 20*456ef56aSSadaf Ebrahimiimport subprocess 21*456ef56aSSadaf Ebrahimi 22*456ef56aSSadaf Ebrahimi"""A regex that captures the JavaDoc comment and function signature.""" 23*456ef56aSSadaf EbrahimiJAVADOC_RPC_REGEX = re.compile( 24*456ef56aSSadaf Ebrahimi # Capture the entire comment string. 25*456ef56aSSadaf Ebrahimi r'(?P<comment>/\*\*(?:(?!/\*\*).)*?\*/)(?:(?:(?!\*/).)*?)\s*' 26*456ef56aSSadaf Ebrahimi # Find at least one @Rpc Annotation 27*456ef56aSSadaf Ebrahimi r'(?:@\w+\s*)*?(?:@Rpc.*?\s*)(?:@\w+\s*)*?' 28*456ef56aSSadaf Ebrahimi # Capture the function signature, ignoring the throws statement 29*456ef56aSSadaf Ebrahimi # (the throws information will be pulled from the comment). 30*456ef56aSSadaf Ebrahimi r'(?P<function_signature>.*?)(?:throws.*?)?{', 31*456ef56aSSadaf Ebrahimi flags=re.MULTILINE | re.DOTALL) 32*456ef56aSSadaf Ebrahimi 33*456ef56aSSadaf Ebrahimi""" 34*456ef56aSSadaf EbrahimiCaptures javadoc "frills" like the ones found below: 35*456ef56aSSadaf Ebrahimi 36*456ef56aSSadaf Ebrahimi/** 37*456ef56aSSadaf Ebrahimi * 38*456ef56aSSadaf Ebrahimi */ 39*456ef56aSSadaf Ebrahimi 40*456ef56aSSadaf Ebrahimi/** */ 41*456ef56aSSadaf Ebrahimi 42*456ef56aSSadaf Ebrahimi""" 43*456ef56aSSadaf EbrahimiCAPTURE_JAVADOC_FRILLS = re.compile( 44*456ef56aSSadaf Ebrahimi r'(^\s*(/\*\*$|/\*\* |\*($| ))|\s*\*/\s*$)', 45*456ef56aSSadaf Ebrahimi re.MULTILINE) 46*456ef56aSSadaf Ebrahimi 47*456ef56aSSadaf Ebrahimi"""A regex to capture the individual pieces of the function signature.""" 48*456ef56aSSadaf EbrahimiCAPTURE_FUNCTION_SIGNATURE = re.compile( 49*456ef56aSSadaf Ebrahimi # Capture any non-static function 50*456ef56aSSadaf Ebrahimi r'(public|private)' 51*456ef56aSSadaf Ebrahimi # Allow synchronized and @Annotations() 52*456ef56aSSadaf Ebrahimi r'[( synchronized)(@\w+\(.*?\)?)]*?' 53*456ef56aSSadaf Ebrahimi # Return Type (Allow n-number of generics and arrays) 54*456ef56aSSadaf Ebrahimi r'(?P<return_type>\w+(?:[\[\]<>\w ,]*?)?)\s+' 55*456ef56aSSadaf Ebrahimi # Capture functionName 56*456ef56aSSadaf Ebrahimi r'(?P<function_name>\w*)\s*' 57*456ef56aSSadaf Ebrahimi # Capture anything enclosed in parens 58*456ef56aSSadaf Ebrahimi r'\((?P<parameters>.*)\)', 59*456ef56aSSadaf Ebrahimi re.MULTILINE | re.DOTALL) 60*456ef56aSSadaf Ebrahimi 61*456ef56aSSadaf Ebrahimi"""Matches a parameter and its RPC annotations.""" 62*456ef56aSSadaf EbrahimiCAPTURE_PARAMETER = re.compile( 63*456ef56aSSadaf Ebrahimi r'(?:' 64*456ef56aSSadaf Ebrahimi r'(?P<optional>@RpcOptional\s+)?' 65*456ef56aSSadaf Ebrahimi r'(?P<rpc_param>@RpcParameter\(.*?\)\s*)?' 66*456ef56aSSadaf Ebrahimi r'(?P<default>@RpcDefault\((?P<default_value>.*)\)\s*)?' 67*456ef56aSSadaf Ebrahimi r')*' 68*456ef56aSSadaf Ebrahimi r'(?P<param_type>\w+)\s+(?P<param_name>\w+)', 69*456ef56aSSadaf Ebrahimi flags=re.MULTILINE | re.DOTALL) 70*456ef56aSSadaf Ebrahimi 71*456ef56aSSadaf Ebrahimi 72*456ef56aSSadaf Ebrahimiclass Facade(object): 73*456ef56aSSadaf Ebrahimi """A class representing a Facade. 74*456ef56aSSadaf Ebrahimi 75*456ef56aSSadaf Ebrahimi Attributes: 76*456ef56aSSadaf Ebrahimi path: the path the facade is located at. 77*456ef56aSSadaf Ebrahimi directory: the 78*456ef56aSSadaf Ebrahimi """ 79*456ef56aSSadaf Ebrahimi 80*456ef56aSSadaf Ebrahimi def __init__(self, path): 81*456ef56aSSadaf Ebrahimi self.path = path 82*456ef56aSSadaf Ebrahimi self.directory = os.path.dirname(self.path) 83*456ef56aSSadaf Ebrahimi # -5 removes the '.java' file extension 84*456ef56aSSadaf Ebrahimi self.name = path[path.rfind('/') + 1:-5] 85*456ef56aSSadaf Ebrahimi self.rpcs = list() 86*456ef56aSSadaf Ebrahimi 87*456ef56aSSadaf Ebrahimi 88*456ef56aSSadaf Ebrahimidef main(): 89*456ef56aSSadaf Ebrahimi basepath = os.path.abspath(os.path.join(os.path.dirname( 90*456ef56aSSadaf Ebrahimi os.path.realpath(__file__)), '..')) 91*456ef56aSSadaf Ebrahimi 92*456ef56aSSadaf Ebrahimi facades = list() 93*456ef56aSSadaf Ebrahimi 94*456ef56aSSadaf Ebrahimi for path, dirs, files in os.walk(basepath): 95*456ef56aSSadaf Ebrahimi for file_name in files: 96*456ef56aSSadaf Ebrahimi if file_name.endswith('Facade.java'): 97*456ef56aSSadaf Ebrahimi facades.append(parse_facade_file(os.path.join(path, file_name))) 98*456ef56aSSadaf Ebrahimi 99*456ef56aSSadaf Ebrahimi basepath = os.path.abspath(os.path.join(os.path.dirname( 100*456ef56aSSadaf Ebrahimi os.path.realpath(__file__)), '..')) 101*456ef56aSSadaf Ebrahimi write_output(facades, os.path.join(basepath, 'Docs/ApiReference.md')) 102*456ef56aSSadaf Ebrahimi 103*456ef56aSSadaf Ebrahimi 104*456ef56aSSadaf Ebrahimidef write_output(facades, output_path): 105*456ef56aSSadaf Ebrahimi facades = sorted(facades, key=lambda x: x.directory) 106*456ef56aSSadaf Ebrahimi 107*456ef56aSSadaf Ebrahimi git_rev = None 108*456ef56aSSadaf Ebrahimi try: 109*456ef56aSSadaf Ebrahimi git_rev = subprocess.check_output('git rev-parse HEAD', 110*456ef56aSSadaf Ebrahimi shell=True).decode('utf-8').strip() 111*456ef56aSSadaf Ebrahimi except subprocess.CalledProcessError as e: 112*456ef56aSSadaf Ebrahimi # Getting the commit ID is optional; we continue if we cannot get it 113*456ef56aSSadaf Ebrahimi pass 114*456ef56aSSadaf Ebrahimi 115*456ef56aSSadaf Ebrahimi with open(output_path, 'w') as fd: 116*456ef56aSSadaf Ebrahimi if git_rev: 117*456ef56aSSadaf Ebrahimi fd.write('Generated at commit `%s`\n\n' % git_rev) 118*456ef56aSSadaf Ebrahimi fd.write('# Facade Groups') 119*456ef56aSSadaf Ebrahimi prev_directory = '' 120*456ef56aSSadaf Ebrahimi for facade in facades: 121*456ef56aSSadaf Ebrahimi if facade.directory != prev_directory: 122*456ef56aSSadaf Ebrahimi fd.write('\n\n## %s\n\n' % facade.directory[ 123*456ef56aSSadaf Ebrahimi facade.directory.rfind('/') + 1:]) 124*456ef56aSSadaf Ebrahimi prev_directory = facade.directory 125*456ef56aSSadaf Ebrahimi fd.write(' * [%s](#%s)\n' % (facade.name, facade.name.lower())) 126*456ef56aSSadaf Ebrahimi 127*456ef56aSSadaf Ebrahimi fd.write('\n# Facades\n\n') 128*456ef56aSSadaf Ebrahimi for facade in facades: 129*456ef56aSSadaf Ebrahimi fd.write('\n## %s' % facade.name) 130*456ef56aSSadaf Ebrahimi for rpc in facade.rpcs: 131*456ef56aSSadaf Ebrahimi fd.write('\n\n### %s\n\n' % rpc.name) 132*456ef56aSSadaf Ebrahimi fd.write('%s\n' % rpc) 133*456ef56aSSadaf Ebrahimi 134*456ef56aSSadaf Ebrahimi 135*456ef56aSSadaf Ebrahimidef parse_facade_file(file_path): 136*456ef56aSSadaf Ebrahimi """Parses a .*Facade.java file and represents it as a Facade object""" 137*456ef56aSSadaf Ebrahimi facade = Facade(file_path) 138*456ef56aSSadaf Ebrahimi with open(file_path, 'r') as content_file: 139*456ef56aSSadaf Ebrahimi content = content_file.read() 140*456ef56aSSadaf Ebrahimi matches = re.findall(JAVADOC_RPC_REGEX, content) 141*456ef56aSSadaf Ebrahimi for match in matches: 142*456ef56aSSadaf Ebrahimi rpc_function = DocumentedFunction( 143*456ef56aSSadaf Ebrahimi match[0].replace('\\n', '\n'), # match[0]: JavaDoc comment 144*456ef56aSSadaf Ebrahimi match[1].replace('\\n', '\n')) # match[1]: function signature 145*456ef56aSSadaf Ebrahimi facade.rpcs.append(rpc_function) 146*456ef56aSSadaf Ebrahimi facade.rpcs.sort(key=lambda rpc: rpc.name) 147*456ef56aSSadaf Ebrahimi return facade 148*456ef56aSSadaf Ebrahimi 149*456ef56aSSadaf Ebrahimi 150*456ef56aSSadaf Ebrahimiclass DefaultValue(object): 151*456ef56aSSadaf Ebrahimi """An object representation of a default value. 152*456ef56aSSadaf Ebrahimi 153*456ef56aSSadaf Ebrahimi Functions as Optional in Java, or a pointer in C++. 154*456ef56aSSadaf Ebrahimi 155*456ef56aSSadaf Ebrahimi Attributes: 156*456ef56aSSadaf Ebrahimi value: the default value 157*456ef56aSSadaf Ebrahimi """ 158*456ef56aSSadaf Ebrahimi def __init__(self, default_value=None): 159*456ef56aSSadaf Ebrahimi self.value = default_value 160*456ef56aSSadaf Ebrahimi 161*456ef56aSSadaf Ebrahimi 162*456ef56aSSadaf Ebrahimiclass DocumentedValue(object): 163*456ef56aSSadaf Ebrahimi def __init__(self): 164*456ef56aSSadaf Ebrahimi """Creates an empty DocumentedValue object.""" 165*456ef56aSSadaf Ebrahimi self.type = 'void' 166*456ef56aSSadaf Ebrahimi self.name = None 167*456ef56aSSadaf Ebrahimi self.description = None 168*456ef56aSSadaf Ebrahimi self.default_value = None 169*456ef56aSSadaf Ebrahimi 170*456ef56aSSadaf Ebrahimi def set_type(self, param_type): 171*456ef56aSSadaf Ebrahimi self.type = param_type 172*456ef56aSSadaf Ebrahimi return self 173*456ef56aSSadaf Ebrahimi 174*456ef56aSSadaf Ebrahimi def set_name(self, name): 175*456ef56aSSadaf Ebrahimi self.name = name 176*456ef56aSSadaf Ebrahimi return self 177*456ef56aSSadaf Ebrahimi 178*456ef56aSSadaf Ebrahimi def set_description(self, description): 179*456ef56aSSadaf Ebrahimi self.description = description 180*456ef56aSSadaf Ebrahimi return self 181*456ef56aSSadaf Ebrahimi 182*456ef56aSSadaf Ebrahimi def set_default_value(self, default_value): 183*456ef56aSSadaf Ebrahimi self.default_value = default_value 184*456ef56aSSadaf Ebrahimi return self 185*456ef56aSSadaf Ebrahimi 186*456ef56aSSadaf Ebrahimi def __str__(self): 187*456ef56aSSadaf Ebrahimi if self.name is None: 188*456ef56aSSadaf Ebrahimi return self.description 189*456ef56aSSadaf Ebrahimi if self.default_value is None: 190*456ef56aSSadaf Ebrahimi return '%s: %s' % (self.name, self.description) 191*456ef56aSSadaf Ebrahimi else: 192*456ef56aSSadaf Ebrahimi return '%s: %s (default: %s)' % (self.name, self.description, 193*456ef56aSSadaf Ebrahimi self.default_value.value) 194*456ef56aSSadaf Ebrahimi 195*456ef56aSSadaf Ebrahimi 196*456ef56aSSadaf Ebrahimiclass DocumentedFunction(object): 197*456ef56aSSadaf Ebrahimi """A combination of all function documentation into a single object. 198*456ef56aSSadaf Ebrahimi 199*456ef56aSSadaf Ebrahimi Attributes: 200*456ef56aSSadaf Ebrahimi _description: A string that describes the function. 201*456ef56aSSadaf Ebrahimi _parameters: A dictionary of {parameter name: DocumentedValue object} 202*456ef56aSSadaf Ebrahimi _return: a DocumentedValue with information on the returned value. 203*456ef56aSSadaf Ebrahimi _throws: A dictionary of {throw type (str): DocumentedValue object} 204*456ef56aSSadaf Ebrahimi 205*456ef56aSSadaf Ebrahimi """ 206*456ef56aSSadaf Ebrahimi def __init__(self, comment, function_signature): 207*456ef56aSSadaf Ebrahimi self._name = None 208*456ef56aSSadaf Ebrahimi self._description = None 209*456ef56aSSadaf Ebrahimi self._parameters = {} 210*456ef56aSSadaf Ebrahimi self._return = DocumentedValue() 211*456ef56aSSadaf Ebrahimi self._throws = {} 212*456ef56aSSadaf Ebrahimi 213*456ef56aSSadaf Ebrahimi self._parse_comment(comment) 214*456ef56aSSadaf Ebrahimi self._parse_function_signature(function_signature) 215*456ef56aSSadaf Ebrahimi 216*456ef56aSSadaf Ebrahimi @property 217*456ef56aSSadaf Ebrahimi def name(self): 218*456ef56aSSadaf Ebrahimi return self._name 219*456ef56aSSadaf Ebrahimi 220*456ef56aSSadaf Ebrahimi def _parse_comment(self, comment): 221*456ef56aSSadaf Ebrahimi """Parses a JavaDoc comment into DocumentedFunction attributes.""" 222*456ef56aSSadaf Ebrahimi comment = str(re.sub(CAPTURE_JAVADOC_FRILLS, '', comment)) 223*456ef56aSSadaf Ebrahimi tag = 'description' 224*456ef56aSSadaf Ebrahimi tag_data = '' 225*456ef56aSSadaf Ebrahimi for line in comment.split('\n'): 226*456ef56aSSadaf Ebrahimi line.strip() 227*456ef56aSSadaf Ebrahimi if line.startswith('@'): 228*456ef56aSSadaf Ebrahimi self._finalize_tag(tag, tag_data) 229*456ef56aSSadaf Ebrahimi tag_end_index = line.find(' ') 230*456ef56aSSadaf Ebrahimi tag = line[1:tag_end_index] 231*456ef56aSSadaf Ebrahimi tag_data = line[tag_end_index + 1:] 232*456ef56aSSadaf Ebrahimi else: 233*456ef56aSSadaf Ebrahimi if not tag_data: 234*456ef56aSSadaf Ebrahimi whitespace_char = '' 235*456ef56aSSadaf Ebrahimi elif (line.startswith(' ') 236*456ef56aSSadaf Ebrahimi or tag_data.endswith('\n') 237*456ef56aSSadaf Ebrahimi or line == ''): 238*456ef56aSSadaf Ebrahimi whitespace_char = '\n' 239*456ef56aSSadaf Ebrahimi else: 240*456ef56aSSadaf Ebrahimi whitespace_char = ' ' 241*456ef56aSSadaf Ebrahimi tag_data = '%s%s%s' % (tag_data, whitespace_char, line) 242*456ef56aSSadaf Ebrahimi self._finalize_tag(tag, tag_data.strip()) 243*456ef56aSSadaf Ebrahimi 244*456ef56aSSadaf Ebrahimi def __str__(self): 245*456ef56aSSadaf Ebrahimi params_signature = ', '.join(['%s %s' % (param.type, param.name) 246*456ef56aSSadaf Ebrahimi for param in self._parameters.values()]) 247*456ef56aSSadaf Ebrahimi params_description = '\n '.join(['%s: %s' % (param.name, 248*456ef56aSSadaf Ebrahimi param.description) 249*456ef56aSSadaf Ebrahimi for param in 250*456ef56aSSadaf Ebrahimi self._parameters.values()]) 251*456ef56aSSadaf Ebrahimi if params_description: 252*456ef56aSSadaf Ebrahimi params_description = ('\n**Parameters:**\n\n %s\n' % 253*456ef56aSSadaf Ebrahimi params_description) 254*456ef56aSSadaf Ebrahimi return_description = '\n' if self._return else '' 255*456ef56aSSadaf Ebrahimi if self._return: 256*456ef56aSSadaf Ebrahimi return_description += ('**Returns:**\n\n %s' % 257*456ef56aSSadaf Ebrahimi self._return.description) 258*456ef56aSSadaf Ebrahimi return ( 259*456ef56aSSadaf Ebrahimi # ReturnType functionName(Parameters) 260*456ef56aSSadaf Ebrahimi '%s %s(%s)\n\n' 261*456ef56aSSadaf Ebrahimi # Description 262*456ef56aSSadaf Ebrahimi '%s\n' 263*456ef56aSSadaf Ebrahimi # Params & Return 264*456ef56aSSadaf Ebrahimi '%s%s' % (self._return.type, self._name, 265*456ef56aSSadaf Ebrahimi params_signature, self._description, 266*456ef56aSSadaf Ebrahimi params_description, return_description)).strip() 267*456ef56aSSadaf Ebrahimi 268*456ef56aSSadaf Ebrahimi def _parse_function_signature(self, function_signature): 269*456ef56aSSadaf Ebrahimi """Parses the function signature into DocumentedFunction attributes.""" 270*456ef56aSSadaf Ebrahimi header_match = re.search(CAPTURE_FUNCTION_SIGNATURE, function_signature) 271*456ef56aSSadaf Ebrahimi self._name = header_match.group('function_name') 272*456ef56aSSadaf Ebrahimi self._return.set_type(header_match.group('return_type')) 273*456ef56aSSadaf Ebrahimi 274*456ef56aSSadaf Ebrahimi for match in re.finditer(CAPTURE_PARAMETER, 275*456ef56aSSadaf Ebrahimi header_match.group('parameters')): 276*456ef56aSSadaf Ebrahimi param_name = match.group('param_name') 277*456ef56aSSadaf Ebrahimi param_type = match.group('param_type') 278*456ef56aSSadaf Ebrahimi if match.group('default_value'): 279*456ef56aSSadaf Ebrahimi default = DefaultValue(match.group('default_value')) 280*456ef56aSSadaf Ebrahimi elif match.group('optional'): 281*456ef56aSSadaf Ebrahimi default = DefaultValue(None) 282*456ef56aSSadaf Ebrahimi else: 283*456ef56aSSadaf Ebrahimi default = None 284*456ef56aSSadaf Ebrahimi 285*456ef56aSSadaf Ebrahimi if param_name in self._parameters: 286*456ef56aSSadaf Ebrahimi param = self._parameters[param_name] 287*456ef56aSSadaf Ebrahimi else: 288*456ef56aSSadaf Ebrahimi param = DocumentedValue() 289*456ef56aSSadaf Ebrahimi param.set_type(param_type) 290*456ef56aSSadaf Ebrahimi param.set_name(param_name) 291*456ef56aSSadaf Ebrahimi param.set_default_value(default) 292*456ef56aSSadaf Ebrahimi 293*456ef56aSSadaf Ebrahimi def _finalize_tag(self, tag, tag_data): 294*456ef56aSSadaf Ebrahimi """Finalize the JavaDoc @tag by adding it to the correct field.""" 295*456ef56aSSadaf Ebrahimi name = tag_data[:tag_data.find(' ')] 296*456ef56aSSadaf Ebrahimi description = tag_data[tag_data.find(' ') + 1:].strip() 297*456ef56aSSadaf Ebrahimi if tag == 'description': 298*456ef56aSSadaf Ebrahimi self._description = tag_data 299*456ef56aSSadaf Ebrahimi elif tag == 'param': 300*456ef56aSSadaf Ebrahimi if name in self._parameters: 301*456ef56aSSadaf Ebrahimi param = self._parameters[name] 302*456ef56aSSadaf Ebrahimi else: 303*456ef56aSSadaf Ebrahimi param = DocumentedValue().set_name(name) 304*456ef56aSSadaf Ebrahimi self._parameters[name] = param 305*456ef56aSSadaf Ebrahimi param.set_description(description) 306*456ef56aSSadaf Ebrahimi elif tag == 'return': 307*456ef56aSSadaf Ebrahimi self._return.set_description(tag_data) 308*456ef56aSSadaf Ebrahimi elif tag == 'throws': 309*456ef56aSSadaf Ebrahimi new_throws = DocumentedValue().set_name(name) 310*456ef56aSSadaf Ebrahimi new_throws.set_description(description) 311*456ef56aSSadaf Ebrahimi self._throws[name] = new_throws 312*456ef56aSSadaf Ebrahimi 313*456ef56aSSadaf Ebrahimi 314*456ef56aSSadaf Ebrahimiif __name__ == '__main__': 315*456ef56aSSadaf Ebrahimi main() 316