text: # 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
    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
        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 = node.id
  
      for template, doc in extracted_docs.output_docs.iteritems():
        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:
        tree = ast.parse(open(bzl_file).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
  
  