普通文本  |  223行  |  7.78 KB

#!/usr/bin/env python
#
#  Copyright (C) 2016 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 collections
import itertools
import os
import re
import subprocess

# Parsing states:
# STATE_INITIAL: looking for rpc or function defintion
# STATE_RPC_DECORATOR: in the middle of a multi-line rpc definition
# STATE_FUNCTION_DECORATOR: in the middle of a multi-line function definition
# STATE_COMPLETE: done parsing a function
STATE_INITIAL = 1
STATE_RPC_DECORATOR = 2
STATE_FUNCTION_DEFINITION = 3
STATE_COMPLETE = 4

# RE to match key=value tuples with matching quoting on value.
KEY_VAL_RE = re.compile(r'''
        (?P<key>\w+)\s*=\s* # Key consists of only alphanumerics
        (?P<quote>["']?)    # Optional quote character.
        (?P<value>.*?)      # Value is a non greedy match
        (?P=quote)          # Closing quote equals the first.
        ($|,)               # Entry ends with comma or end of string
    ''', re.VERBOSE)

# RE to match a function definition and extract out the function name.
FUNC_RE = re.compile(r'.+\s+(\w+)\s*\(.*')


class Function(object):
    """Represents a RPC-exported function."""

    def __init__(self, rpc_def, func_def):
        """Constructs a function object given its RPC and function signature."""
        self._function = ''
        self._signature = ''
        self._description = ''
        self._returns = ''

        self._ParseRpcDefinition(rpc_def)
        self._ParseFunctionDefinition(func_def)

    def _ParseRpcDefinition(self, s):
        """Parse RPC definition."""
        # collapse string concatenation
        s = s.replace('" + "', '')
        s = s.strip('()')
        for m in KEY_VAL_RE.finditer(s):
            if m.group('key') == 'description':
                self._description = m.group('value')
            if m.group('key') == 'returns':
                self._returns = m.group('value')

    def _ParseFunctionDefinition(self, s):
        """Parse function definition."""
        # Remove some keywords we don't care about.
        s = s.replace('public ', '')
        s = s.replace('synchronized ', '')
        # Remove any throw specifications.
        s = re.sub('\s+throws.*', '', s)
        s = s.strip('{')
        # Remove all the RPC parameter annotations.
        s = s.replace('@RpcOptional ', '')
        s = s.replace('@RpcOptional() ', '')
        s = re.sub('@RpcParameter\s*\(.+?\)\s+', '', s)
        s = re.sub('@RpcDefault\s*\(.+?\)\s+', '', s)
        m = FUNC_RE.match(s)
        if m:
            self._function = m.group(1)
        self._signature = s.strip()

    @property
    def function(self):
        return self._function

    @property
    def signature(self):
        return self._signature

    @property
    def description(self):
        return self._description

    @property
    def returns(self):
        return self._returns


class DocGenerator(object):
    """Documentation genereator."""

    def __init__(self, basepath):
        """Construct based on all the *Facade.java files in the given basepath."""
        self._functions = collections.defaultdict(list)

        for path, dirs, files in os.walk(basepath):
            for f in files:
                if f.endswith('Facade.java'):
                    self._Parse(os.path.join(path, f))

    def _Parse(self, filename):
        """Parser state machine for a single file."""
        state = STATE_INITIAL
        self._current_rpc = ''
        self._current_function = ''

        with open(filename, 'r') as f:
            for line in f.readlines():
                line = line.strip()
                if state == STATE_INITIAL:
                    state = self._ParseLineInitial(line)
                elif state == STATE_RPC_DECORATOR:
                    state = self._ParseLineRpcDecorator(line)
                elif state == STATE_FUNCTION_DEFINITION:
                    state = self._ParseLineFunctionDefinition(line)

                if state == STATE_COMPLETE:
                    self._EmitFunction(filename)
                    state = STATE_INITIAL

    def _ParseLineInitial(self, line):
        """Parse a line while in STATE_INITIAL."""
        if line.startswith('@Rpc('):
            self._current_rpc = line[4:]
            if not line.endswith(')'):
                # Multi-line RPC definition
                return STATE_RPC_DECORATOR
        elif line.startswith('public'):
            self._current_function = line
            if not line.endswith('{'):
                # Multi-line function definition
                return STATE_FUNCTION_DEFINITION
            else:
                return STATE_COMPLETE
        return STATE_INITIAL

    def _ParseLineRpcDecorator(self, line):
        """Parse a line while in STATE_RPC_DECORATOR."""
        self._current_rpc += ' ' + line
        if line.endswith(')'):
            # Done with RPC definition
            return STATE_INITIAL
        else:
            # Multi-line RPC definition
            return STATE_RPC_DECORATOR

    def _ParseLineFunctionDefinition(self, line):
        """Parse a line while in STATE_FUNCTION_DEFINITION."""
        self._current_function += ' ' + line
        if line.endswith('{'):
            # Done with function definition
            return STATE_COMPLETE
        else:
            # Multi-line function definition
            return STATE_FUNCTION_DEFINITION

    def _EmitFunction(self, filename):
        """Store a function definition from the current parse state."""
        if self._current_rpc and self._current_function:
            module = os.path.basename(filename)[0:-5]
            f = Function(self._current_rpc, self._current_function)
            if f.function:
                self._functions[module].append(f)

        self._current_rpc = None
        self._current_function = None

    def WriteOutput(self, filename):
        git_rev = None
        try:
            git_rev = subprocess.check_output('git rev-parse HEAD',
                                              shell=True).strip()
        except subprocess.CalledProcessError as e:
            # Getting the commit ID is optional; we continue if we cannot get it
            pass

        with open(filename, 'w') as f:
            if git_rev:
                f.write('Generated at commit `%s`\n\n' % git_rev)
            # Write table of contents
            for module in sorted(self._functions.keys()):
                f.write('**%s**\n\n' % module)
                for func in self._functions[module]:
                    f.write('  * [%s](#%s)\n' %
                            (func.function, func.function.lower()))
                f.write('\n')

            f.write('# Method descriptions\n\n')
            for func in itertools.chain.from_iterable(
                    self._functions.itervalues()):
                f.write('## %s\n\n' % func.function)
                f.write('```\n')
                f.write('%s\n\n' % func.signature)
                f.write('%s\n' % func.description)
                if func.returns:
                    if func.returns.lower().startswith('return'):
                        f.write('\n%s\n' % func.returns)
                    else:
                        f.write('\nReturns %s\n' % func.returns)
                f.write('```\n\n')

# Main
basepath = os.path.abspath(os.path.join(os.path.dirname(
    os.path.realpath(__file__)), '..'))
g = DocGenerator(basepath)
g.WriteOutput(os.path.join(basepath, 'Docs/ApiReference.md'))