blob: 0be97090d86a30d1003fd960c5852b9e7838d9e3 [file] [log] [blame]
# Copyright 2016 The Bazel Authors. All rights reserved.
#
# 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.
"""Extractor for Skylark macro documentation."""
import ast
# internal imports
from skydoc import build_pb2
from skydoc import common
def get_type(expr):
"""Find the type of an expression.
Args:
expr: The expression to check.
Returns:
The type of the expression.
"""
if isinstance(expr, ast.Num):
return build_pb2.Attribute.INTEGER
elif isinstance(expr, ast.Str):
return build_pb2.Attribute.STRING
elif isinstance(expr, ast.List):
return build_pb2.Attribute.STRING_LIST
elif isinstance(expr, ast.Name) and (expr.id == "True" or expr.id == "False"):
return build_pb2.Attribute.BOOLEAN
elif hasattr(ast, 'NameConstant') and isinstance(expr, ast.NameConstant) and (expr.value == True or expr.value == False):
return build_pb2.Attribute.BOOLEAN
else:
return build_pb2.Attribute.UNKNOWN
class MacroDocExtractor(object):
"""Extracts documentation for macros from a .bzl file"""
def __init__(self):
"""Inits MacroDocExtractor with a new BuildLanguage proto"""
self.__language = build_pb2.BuildLanguage()
self.title = ""
self.description = ""
def _add_file_docs(self, tree):
"""Extracts the file docstring of the .bzl file."""
docstring = ast.get_docstring(tree)
if docstring == None:
return
lines = docstring.split("\n")
i = 0
for line in lines:
if line != '':
i = i + 1
else:
break
self.title = " ".join(lines[:i])
self.description = "\n".join(lines[i + 1:])
def _add_macro_doc(self, stmt):
# The defaults array contains default values for the last arguments.
# The first shift arguments are mandatory.
shift = len(stmt.args.args) - len(stmt.args.defaults)
rule = self.__language.rule.add()
rule.name = stmt.name
rule.type = build_pb2.RuleDefinition.MACRO
doc = ast.get_docstring(stmt)
if doc:
extracted_docs = common.parse_docstring(doc)
rule.documentation = extracted_docs.doc
if extracted_docs.example_doc:
rule.example_documentation = extracted_docs.example_doc
else:
extracted_docs = common.ExtractedDocs(
doc="", attr_docs={}, example_doc="", output_docs={})
for i in range(len(stmt.args.args)):
attr = rule.attribute.add()
attr_name = stmt.args.args[i].id if hasattr(stmt.args.args[i], 'id') else stmt.args.args[i].arg
attr.name = attr_name
if attr_name in extracted_docs.attr_docs:
attr.documentation = extracted_docs.attr_docs[attr_name]
if i < shift: # The first arguments are mandatory
attr.mandatory = True
attr.type = build_pb2.Attribute.UNKNOWN
else:
node = stmt.args.defaults[i - shift]
attr.mandatory = False
attr.type = get_type(node)
if attr.type == build_pb2.Attribute.BOOLEAN:
attr.default = str(node.value) if hasattr(node, 'value') else node.id
if stmt.args.kwarg:
attr = rule.attribute.add()
attr_name = '**' + (stmt.args.kwarg.arg if hasattr(stmt.args.kwarg, 'arg') else stmt.args.kwarg)
attr.name = attr_name
attr.mandatory = False
attr.type = build_pb2.Attribute.UNKNOWN
if attr_name in extracted_docs.attr_docs:
attr.documentation = extracted_docs.attr_docs[attr_name]
for template, doc in extracted_docs.output_docs.items():
output = rule.output.add()
output.template = template
output.documentation = doc
def parse_bzl(self, bzl_file):
"""Extracts documentation for all public macros from the given .bzl file.
Args:
bzl_file: The .bzl file to extract macro documentation from.
"""
try:
with open(bzl_file) as f:
tree = ast.parse(f.read(), bzl_file)
self._add_file_docs(tree)
for stmt in tree.body:
if isinstance(stmt, ast.FunctionDef) and not stmt.name.startswith("_"):
self._add_macro_doc(stmt)
except IOError as e:
# Ignore missing extension
print("Failed to parse {0}: {1}".format(bzl_file, e.strerror))
pass
def proto(self):
"""Returns the proto containing the macro documentation."""
return self.__language