Support user-defined function documentation in Stardoc.

RELNOTES: None.
PiperOrigin-RevId: 220525539
diff --git a/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java b/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
index dfc7a4a..7a15a51 100644
--- a/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
+++ b/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
@@ -40,6 +40,7 @@
 import com.google.devtools.build.lib.syntax.ParserInputSource;
 import com.google.devtools.build.lib.syntax.Runtime;
 import com.google.devtools.build.lib.syntax.SkylarkImport;
+import com.google.devtools.build.lib.syntax.UserDefinedFunction;
 import com.google.devtools.build.skydoc.fakebuildapi.FakeActionsInfoProvider;
 import com.google.devtools.build.skydoc.fakebuildapi.FakeBuildApiGlobals;
 import com.google.devtools.build.skydoc.fakebuildapi.FakeConfigApi;
@@ -72,6 +73,8 @@
 import com.google.devtools.build.skydoc.rendering.MarkdownRenderer;
 import com.google.devtools.build.skydoc.rendering.ProviderInfo;
 import com.google.devtools.build.skydoc.rendering.RuleInfo;
+import com.google.devtools.build.skydoc.rendering.UserDefinedFunctionInfo;
+import com.google.devtools.build.skydoc.rendering.UserDefinedFunctionInfo.DocstringParseException;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.nio.file.NoSuchFileException;
@@ -83,6 +86,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.TreeMap;
 import java.util.stream.Collectors;
 
 /**
@@ -132,9 +136,11 @@
     ImmutableMap.Builder<String, RuleInfo> ruleInfoMap = ImmutableMap.builder();
     ImmutableMap.Builder<String, ProviderInfo> providerInfoMap = ImmutableMap.builder();
     ImmutableList.Builder<RuleInfo> unknownNamedRules = ImmutableList.builder();
+    ImmutableMap.Builder<String, UserDefinedFunction> userDefinedFunctions = ImmutableMap.builder();
 
     new SkydocMain(new FilesystemFileAccessor())
-        .eval(targetFileLabel, ruleInfoMap, unknownNamedRules, providerInfoMap);
+        .eval(
+            targetFileLabel, ruleInfoMap, unknownNamedRules, providerInfoMap, userDefinedFunctions);
 
     MarkdownRenderer renderer = new MarkdownRenderer();
 
@@ -142,6 +148,7 @@
       try (PrintWriter printWriter = new PrintWriter(outputPath, "UTF-8")) {
         printRuleInfos(printWriter, renderer, ruleInfoMap.build(), unknownNamedRules.build());
         printProviderInfos(printWriter, renderer, providerInfoMap.build());
+        printUserDefinedFunctions(printWriter, renderer, userDefinedFunctions.build());
       }
     } else {
       Map<String, RuleInfo> filteredRuleInfos =
@@ -152,9 +159,14 @@
           providerInfoMap.build().entrySet().stream()
               .filter(entry -> symbolNames.contains(entry.getKey()))
               .collect(ImmutableMap.toImmutableMap(Entry::getKey, Entry::getValue));
+      Map<String, UserDefinedFunction> filteredUserDefinedFunctions =
+          userDefinedFunctions.build().entrySet().stream()
+              .filter(entry -> symbolNames.contains(entry.getKey()))
+              .collect(ImmutableMap.toImmutableMap(Entry::getKey, Entry::getValue));
       try (PrintWriter printWriter = new PrintWriter(outputPath, "UTF-8")) {
         printRuleInfos(printWriter, renderer, filteredRuleInfos, ImmutableList.of());
         printProviderInfos(printWriter, renderer, filteredProviderInfos);
+        printUserDefinedFunctions(printWriter, renderer, filteredUserDefinedFunctions);
       }
     }
   }
@@ -192,6 +204,24 @@
     }
   }
 
+  private static void printUserDefinedFunctions(
+      PrintWriter printWriter,
+      MarkdownRenderer renderer,
+      Map<String, UserDefinedFunction> userDefinedFunctions)
+      throws IOException {
+    for (Entry<String, UserDefinedFunction> entry : userDefinedFunctions.entrySet()) {
+      try {
+        UserDefinedFunctionInfo functionInfo =
+            UserDefinedFunctionInfo.fromNameAndFunction(entry.getKey(), entry.getValue());
+        printUserDefinedFunctionInfo(printWriter, renderer, functionInfo);
+        printWriter.println();
+      } catch (DocstringParseException exception) {
+        System.err.println(exception.getMessage());
+        System.err.println();
+      }
+    }
+  }
+
   private static void printRuleInfo(
       PrintWriter printWriter, MarkdownRenderer renderer,
       String exportedName, RuleInfo ruleInfo) throws IOException {
@@ -204,6 +234,12 @@
     printWriter.println(renderer.render(exportedName, providerInfo));
   }
 
+  private static void printUserDefinedFunctionInfo(
+      PrintWriter printWriter, MarkdownRenderer renderer, UserDefinedFunctionInfo functionInfo)
+      throws IOException {
+    printWriter.println(renderer.render(functionInfo));
+  }
+
   /**
    * Evaluates/interprets the skylark file at a given path and its transitive skylark dependencies
    * using a fake build API and collects information about all rule definitions made in the root
@@ -216,16 +252,20 @@
    * @param unknownNamedRules a list builder to be populated with rule definition information for
    *     rules which were not exported as top level symbols
    * @param providerInfoMap a map builder to be populated with provider definition information for
-   *     named providers. Keys are exported names of providers, and values are their
-   *     {@link ProviderInfo} descriptions. For example, 'my_provider = provider(...)' has key
+   *     named providers. Keys are exported names of providers, and values are their {@link
+   *     ProviderInfo} descriptions. For example, 'my_provider = provider(...)' has key
    *     'my_provider'
+   * @param userDefinedFunctionMap a map builder to be populated with user-defined functions. Keys
+   *     are exported names of functions, and values are the {@link UserDefinedFunction} objects.
+   *     For example, 'def my_function(foo):' is a function with key 'my_function'.
    * @throws InterruptedException if evaluation is interrupted
    */
   public Environment eval(
       Label label,
       ImmutableMap.Builder<String, RuleInfo> ruleInfoMap,
       ImmutableList.Builder<RuleInfo> unknownNamedRules,
-      ImmutableMap.Builder<String, ProviderInfo> providerInfoMap)
+      ImmutableMap.Builder<String, ProviderInfo> providerInfoMap,
+      ImmutableMap.Builder<String, UserDefinedFunction> userDefinedFunctionMap)
       throws InterruptedException, IOException, LabelSyntaxException {
 
     List<RuleInfo> ruleInfoList = new ArrayList<>();
@@ -242,7 +282,11 @@
             Functions.identity()));
 
     ImmutableSet.Builder<RuleInfo> handledRuleDefinitions = ImmutableSet.builder();
-    for (Entry<String, Object> envEntry : env.getGlobals().getBindings().entrySet()) {
+
+    // Sort the bindings so their ordering is deterministic.
+    TreeMap<String, Object> sortedBindings = new TreeMap<>(env.getGlobals().getBindings());
+
+    for (Entry<String, Object> envEntry : sortedBindings.entrySet()) {
       if (ruleFunctions.containsKey(envEntry.getValue())) {
         RuleInfo ruleInfo = ruleFunctions.get(envEntry.getValue());
         ruleInfoMap.put(envEntry.getKey(), ruleInfo);
@@ -252,6 +296,10 @@
         ProviderInfo providerInfo = providerInfos.get(envEntry.getValue());
         providerInfoMap.put(envEntry.getKey(), providerInfo);
       }
+      if (envEntry.getValue() instanceof UserDefinedFunction) {
+        UserDefinedFunction userDefinedFunction = (UserDefinedFunction) envEntry.getValue();
+        userDefinedFunctionMap.put(envEntry.getKey(), userDefinedFunction);
+      }
     }
 
     unknownNamedRules.addAll(ruleFunctions.values().stream()
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeStructApi.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeStructApi.java
index abdea38..cd99533 100644
--- a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeStructApi.java
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeStructApi.java
@@ -51,6 +51,8 @@
     return "";
   }
 
+  // TODO(cparsons): Implement repr to match the real Struct's repr, as it affects the
+  // "default value" documentation of functions.
   @Override
   public void repr(SkylarkPrinter printer) {}
 
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/BUILD b/src/main/java/com/google/devtools/build/skydoc/rendering/BUILD
index 6a2633b..731ec46 100644
--- a/src/main/java/com/google/devtools/build/skydoc/rendering/BUILD
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/BUILD
@@ -17,6 +17,7 @@
         "//src/main/java/com/google/devtools/build/lib:events",
         "//src/main/java/com/google/devtools/build/lib:skylarkinterface",
         "//src/main/java/com/google/devtools/build/lib:syntax",
+        "//src/tools/skylark/java/com/google/devtools/skylark/skylint:skylint_lib",
         "//third_party:apache_velocity",
         "//third_party:guava",
         "//third_party:jsr305",
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/FunctionParamInfo.java b/src/main/java/com/google/devtools/build/skydoc/rendering/FunctionParamInfo.java
new file mode 100644
index 0000000..62866ef
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/FunctionParamInfo.java
@@ -0,0 +1,79 @@
+// Copyright 2018 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.
+
+package com.google.devtools.build.skydoc.rendering;
+
+import com.google.devtools.build.lib.syntax.Printer;
+import com.google.devtools.build.lib.syntax.Printer.BasePrinter;
+import javax.annotation.Nullable;
+
+/** Stores information about a function parameter definition. */
+public class FunctionParamInfo {
+
+  private final String name;
+  private final String docString;
+  @Nullable private final Object defaultValue;
+
+  public FunctionParamInfo(String name, String docString, @Nullable Object defaultValue) {
+    this.name = name;
+    this.docString = docString;
+    this.defaultValue = defaultValue;
+  }
+
+  /**
+   * Return the name of this parameter (for example, in 'def foo(bar):', the only parameter is
+   * named 'bar'.
+   */
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Return the documented description of this parameter (if specified in the function's docstring).
+   */
+  public String getDocString() {
+    return docString;
+  }
+
+  /**
+   * Returns true if this function has a default value and the default value can be displayed
+   * as a string.
+   */
+  public boolean hasDefaultValueString() {
+    return defaultValue != null && !getDefaultString().isEmpty();
+  }
+
+  /**
+   * Returns a string representing the default value this function parameter.
+   *
+   * @throws IllegalStateException if there is no default value of this function parameter;
+   *     invoke {@link #hasDefaultValueString()} first to check whether there is a default
+   *     parameter
+   */
+  public String getDefaultString() {
+    if (defaultValue == null) {
+      return "";
+    }
+    BasePrinter printer = Printer.getSimplifiedPrinter();
+    printer.repr(defaultValue);
+    return printer.toString();
+  }
+
+  /**
+   * Returns 'required' if this parameter is mandatory, otherwise returns 'optional'.
+   */
+  public String getMandatoryString() {
+    return defaultValue == null ? "required" : "optional";
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownRenderer.java b/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownRenderer.java
index 9c82cde..a5f79b3 100644
--- a/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownRenderer.java
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownRenderer.java
@@ -36,6 +36,8 @@
       "com/google/devtools/build/skydoc/rendering/templates/rule.vm";
   private static final String PROVIDER_TEMPLATE_FILENAME =
       "com/google/devtools/build/skydoc/rendering/templates/provider.vm";
+  private static final String FUNCTION_TEMPLATE_FILENAME =
+      "com/google/devtools/build/skydoc/rendering/templates/func.vm";
 
   private final VelocityEngine velocityEngine;
 
@@ -88,6 +90,23 @@
     return stringWriter.toString();
   }
 
+  /**
+   * Returns a markdown rendering of a user-defined function's documentation for the function info
+   * object.
+   */
+  public String render(UserDefinedFunctionInfo functionInfo) throws IOException {
+    VelocityContext context = new VelocityContext();
+    context.put("funcInfo", functionInfo);
+
+    StringWriter stringWriter = new StringWriter();
+    try {
+      velocityEngine.mergeTemplate(FUNCTION_TEMPLATE_FILENAME, "UTF-8", context, stringWriter);
+    } catch (ResourceNotFoundException | ParseErrorException | MethodInvocationException e) {
+      throw new IOException(e);
+    }
+    return stringWriter.toString();
+  }
+
   private static String getSummaryForm(String ruleName, RuleInfo ruleInfo) {
     List<String> attributeNames = ruleInfo.getAttributes().stream()
         .map(attr -> attr.getName())
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/UserDefinedFunctionInfo.java b/src/main/java/com/google/devtools/build/skydoc/rendering/UserDefinedFunctionInfo.java
new file mode 100644
index 0000000..4ee8a4b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/UserDefinedFunctionInfo.java
@@ -0,0 +1,184 @@
+// Copyright 2018 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.
+
+package com.google.devtools.build.skydoc.rendering;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.syntax.FunctionSignature;
+import com.google.devtools.build.lib.syntax.SkylarkType;
+import com.google.devtools.build.lib.syntax.StringLiteral;
+import com.google.devtools.build.lib.syntax.UserDefinedFunction;
+import com.google.devtools.skylark.skylint.DocstringUtils;
+import com.google.devtools.skylark.skylint.DocstringUtils.DocstringInfo;
+import com.google.devtools.skylark.skylint.DocstringUtils.DocstringParseError;
+import com.google.devtools.skylark.skylint.DocstringUtils.ParameterDoc;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/** Encapsulates information about a user-defined Starlark function. */
+public class UserDefinedFunctionInfo {
+
+  private final String functionName;
+  private final Collection<FunctionParamInfo> parameters;
+  private final String docString;
+
+  /**
+   * An exception that may be thrown during construction of {@link UserDefinedFunctionInfo} if the
+   * function's docstring is malformed.
+   */
+  public static class DocstringParseException extends Exception {
+    public DocstringParseException(
+        String functionName, Location definedLocation, List<DocstringParseError> parseErrors) {
+      super(getMessage(functionName, definedLocation, parseErrors));
+    }
+
+    private static String getMessage(
+        String functionName, Location definedLocation, List<DocstringParseError> parseErrors) {
+      StringBuilder message = new StringBuilder();
+      message.append(
+          String.format(
+              "Unable to generate documentation for function %s (defined at %s) "
+                  + "due to malformed docstring. Parse errors:\n",
+              functionName, definedLocation));
+      for (DocstringParseError parseError : parseErrors) {
+        message.append(
+            String.format(
+                "  %s line %s: %s\n",
+                definedLocation,
+                parseError.getLineNumber(),
+                parseError.getMessage().replace('\n', ' ')));
+      }
+      return message.toString();
+    }
+  }
+
+  /**
+   * Create and return a {@link UserDefinedFunctionInfo} object encapsulating information obtained
+   * from the given function and from its parsed docstring.
+   *
+   * @param functionName the name of the function in the target scope. (Note this is not necessarily
+   *     the original exported function name; the function may have been renamed in the target
+   *     Starlark file's scope)
+   * @param userDefinedFunction the raw function object
+   * @throws DocstringParseException if the function's docstring is malformed
+   */
+  public static UserDefinedFunctionInfo fromNameAndFunction(
+      String functionName, UserDefinedFunction userDefinedFunction) throws DocstringParseException {
+    String functionDescription = "";
+    Map<String, String> paramNameToDocMap = Maps.newLinkedHashMap();
+
+    StringLiteral docStringLiteral =
+        DocstringUtils.extractDocstring(userDefinedFunction.getStatements());
+
+    if (docStringLiteral != null) {
+      List<DocstringParseError> parseErrors = Lists.newArrayList();
+      DocstringInfo docstringInfo = DocstringUtils.parseDocstring(docStringLiteral, parseErrors);
+      if (!parseErrors.isEmpty()) {
+        throw new DocstringParseException(
+            functionName, userDefinedFunction.getLocation(), parseErrors);
+      }
+      functionDescription += docstringInfo.getSummary();
+      if (!docstringInfo.getSummary().isEmpty() && !docstringInfo.getLongDescription().isEmpty()) {
+        functionDescription += "\n\n";
+      }
+      functionDescription += docstringInfo.getLongDescription();
+      for (ParameterDoc paramDoc : docstringInfo.getParameters()) {
+        paramNameToDocMap.put(paramDoc.getParameterName(), paramDoc.getDescription());
+      }
+    }
+
+    return new UserDefinedFunctionInfo(
+        functionName,
+        parameterInfos(userDefinedFunction, paramNameToDocMap),
+        functionDescription);
+  }
+
+  private static List<FunctionParamInfo> parameterInfos(
+      UserDefinedFunction userDefinedFunction,
+      Map<String, String> paramNameToDocMap)  {
+    FunctionSignature.WithValues<Object, SkylarkType> signature =
+        userDefinedFunction.getSignature();
+    ImmutableList.Builder<FunctionParamInfo> parameterInfos = ImmutableList.builder();
+
+    List<String> paramNames = signature.getSignature().getNames();
+    // Mandatory parameters must always come before optional parameters, so this counts
+    // down until all mandatory parameters have been exhausted, and then starts filling in
+    // the default parameters accordingly.
+    int numMandatoryParamsLeft =
+        signature.getDefaultValues() != null
+            ? paramNames.size() - signature.getDefaultValues().size()
+            : paramNames.size();
+    int optionalParamIndex = 0;
+
+    for (String paramName : paramNames) {
+      Object defaultParamValue = null;
+      String paramDoc = "";
+      if (numMandatoryParamsLeft == 0) {
+        defaultParamValue = signature.getDefaultValues().get(optionalParamIndex);
+        optionalParamIndex++;
+      } else {
+        numMandatoryParamsLeft--;
+      }
+      if (paramNameToDocMap.containsKey(paramName)) {
+        paramDoc = paramNameToDocMap.get(paramName);
+      }
+      parameterInfos.add(new FunctionParamInfo(paramName, paramDoc, defaultParamValue));
+    }
+    return parameterInfos.build();
+  }
+
+  private UserDefinedFunctionInfo(
+      String functionName, Collection<FunctionParamInfo> parameters, String docString) {
+    this.functionName = functionName;
+    this.parameters = parameters;
+    this.docString = docString;
+  }
+
+  /** Returns the raw name of this function, for example, "my_function". */
+  public String getName() {
+    return functionName;
+  }
+
+  /**
+   * Returns a collection of {@link FunctionParamInfo} objects, where each encapsulates information
+   * about what of the function parameters. Ordering matches the actual Starlark function signature.
+   */
+  public Collection<FunctionParamInfo> getParameters() {
+    return parameters;
+  }
+
+  /** Returns the summary form string this function, for example, "my_function(foo, bar)". */
+  // TODO(cparsons): Compute summary form in the markdown template, as there should be links
+  // between the summary's parameter names and their corresponding documentation.
+  @SuppressWarnings("unused") // Used by markdown template.
+  public String getSummaryForm() {
+    List<String> paramNames =
+        parameters.stream().map(param -> param.getName()).collect(Collectors.toList());
+    return functionName + "(" + Joiner.on(", ").join(paramNames) + ")";
+  }
+
+  /**
+   * Returns the portion of the docstring that is not part of any special sections, such as "Args:"
+   * or "Returns:". Returns the empty string if there is no docstring literal for this function.
+   */
+  public String getDocString() {
+    return docString;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/templates/func.vm b/src/main/java/com/google/devtools/build/skydoc/rendering/templates/func.vm
new file mode 100644
index 0000000..b488b98
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/templates/func.vm
@@ -0,0 +1,34 @@
+#[[##]]# ${funcInfo.name}
+
+<pre>
+${funcInfo.summaryForm}
+</pre>
+
+${funcInfo.docString}
+
+#if (!$funcInfo.parameters.isEmpty())
+#[[###]]# Parameters
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+#foreach ($param in $funcInfo.parameters)
+    <tr>
+      <td><code>${param.name}</code></td>
+      <td>
+        ${param.mandatoryString}.#if($param.hasDefaultValueString()) default is <code>${param.defaultString}</code>#end
+
+#if (!$param.docString.isEmpty())
+        <p>
+          ${param.docString}
+        </p>
+#end
+      </td>
+    </tr>
+#end
+  </tbody>
+</table>
+#end
diff --git a/src/test/java/com/google/devtools/build/skydoc/BUILD b/src/test/java/com/google/devtools/build/skydoc/BUILD
index ceb6eac..67f68c1 100644
--- a/src/test/java/com/google/devtools/build/skydoc/BUILD
+++ b/src/test/java/com/google/devtools/build/skydoc/BUILD
@@ -40,6 +40,7 @@
     golden_file = "testdata/simple_test/golden.txt",
     input_file = "testdata/simple_test/input.bzl",
     skydoc = "//src/main/java/com/google/devtools/build/skydoc",
+    whitelisted_symbols = ["my_rule"],
 )
 
 skydoc_test(
@@ -61,6 +62,7 @@
     golden_file = "testdata/android_basic_test/golden.txt",
     input_file = "testdata/android_basic_test/input.bzl",
     skydoc = "//src/main/java/com/google/devtools/build/skydoc",
+    whitelisted_symbols = ["android_related_rule"],
 )
 
 skydoc_test(
@@ -68,6 +70,7 @@
     golden_file = "testdata/apple_basic_test/golden.txt",
     input_file = "testdata/apple_basic_test/input.bzl",
     skydoc = "//src/main/java/com/google/devtools/build/skydoc",
+    whitelisted_symbols = ["apple_related_rule"],
 )
 
 skydoc_test(
@@ -75,6 +78,7 @@
     golden_file = "testdata/cpp_basic_test/golden.txt",
     input_file = "testdata/cpp_basic_test/input.bzl",
     skydoc = "//src/main/java/com/google/devtools/build/skydoc",
+    whitelisted_symbols = ["cpp_related_rule"],
 )
 
 skydoc_test(
@@ -82,6 +86,7 @@
     golden_file = "testdata/java_basic_test/golden.txt",
     input_file = "testdata/java_basic_test/input.bzl",
     skydoc = "//src/main/java/com/google/devtools/build/skydoc",
+    whitelisted_symbols = ["java_related_rule"],
 )
 
 skydoc_test(
@@ -96,6 +101,17 @@
 )
 
 skydoc_test(
+    name = "same_level_file_test",
+    golden_file = "//src/test/java/com/google/devtools/build/skydoc/testdata/same_level_file_test:golden.txt",
+    input_file = "//src/test/java/com/google/devtools/build/skydoc/testdata/same_level_file_test:input.bzl",
+    skydoc = "//src/main/java/com/google/devtools/build/skydoc",
+    whitelisted_symbols = ["my_rule"],
+    deps = [
+        "//src/test/java/com/google/devtools/build/skydoc/testdata/same_level_file_test:dep.bzl",
+    ],
+)
+
+skydoc_test(
     name = "misc_apis_test",
     golden_file = "testdata/misc_apis_test/golden.txt",
     input_file = "testdata/misc_apis_test/input.bzl",
@@ -107,6 +123,7 @@
     golden_file = "testdata/attribute_types_test/golden.txt",
     input_file = "testdata/attribute_types_test/input.bzl",
     skydoc = "//src/main/java/com/google/devtools/build/skydoc",
+    whitelisted_symbols = ["my_rule"],
 )
 
 skydoc_test(
@@ -129,3 +146,10 @@
     input_file = "testdata/provider_basic_test/input.bzl",
     skydoc = "//src/main/java/com/google/devtools/build/skydoc",
 )
+
+skydoc_test(
+    name = "function_basic_test",
+    golden_file = "testdata/function_basic_test/golden.txt",
+    input_file = "testdata/function_basic_test/input.bzl",
+    skydoc = "//src/main/java/com/google/devtools/build/skydoc",
+)
diff --git a/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java b/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java
index cbdc2d3..be440b0 100644
--- a/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java
+++ b/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java
@@ -23,10 +23,13 @@
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.skylark.util.SkylarkTestCase;
 import com.google.devtools.build.lib.syntax.ParserInputSource;
+import com.google.devtools.build.lib.syntax.UserDefinedFunction;
 import com.google.devtools.build.lib.vfs.FileSystemUtils;
 import com.google.devtools.build.lib.vfs.Path;
 import com.google.devtools.build.skydoc.fakebuildapi.FakeDescriptor.Type;
 import com.google.devtools.build.skydoc.rendering.RuleInfo;
+import com.google.devtools.build.skydoc.rendering.UserDefinedFunctionInfo;
+import com.google.devtools.build.skydoc.rendering.UserDefinedFunctionInfo.DocstringParseException;
 import java.io.IOException;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -82,6 +85,7 @@
         Label.parseAbsoluteUnchecked("//test:test.bzl"),
         ruleInfoMap,
         unexportedRuleInfos,
+        ImmutableMap.builder(),
         ImmutableMap.builder());
     Map<String, RuleInfo> ruleInfos = ruleInfoMap.build();
     assertThat(ruleInfos).hasSize(1);
@@ -144,6 +148,7 @@
         Label.parseAbsoluteUnchecked("//test:test.bzl"),
         ruleInfoMap,
         unexportedRuleInfos,
+        ImmutableMap.builder(),
         ImmutableMap.builder());
 
     assertThat(ruleInfoMap.build().keySet()).containsExactly("rule_one", "rule_two");
@@ -196,6 +201,7 @@
         Label.parseAbsoluteUnchecked("//test:main.bzl"),
         ruleInfoMapBuilder,
         ImmutableList.builder(),
+        ImmutableMap.builder(),
         ImmutableMap.builder());
 
     Map<String, RuleInfo> ruleInfoMap = ruleInfoMapBuilder.build();
@@ -249,6 +255,7 @@
         Label.parseAbsoluteUnchecked("//test:main.bzl"),
         ruleInfoMapBuilder,
         ImmutableList.builder(),
+        ImmutableMap.builder(),
         ImmutableMap.builder());
 
     Map<String, RuleInfo> ruleInfoMap = ruleInfoMapBuilder.build();
@@ -282,13 +289,62 @@
     ImmutableMap.Builder<String, RuleInfo> ruleInfoMapBuilder = ImmutableMap.builder();
 
     IllegalStateException expected =
-        assertThrows(IllegalStateException.class,
-            () -> skydocMain.eval(
-                Label.parseAbsoluteUnchecked("//test:main.bzl"),
-                ruleInfoMapBuilder,
-                ImmutableList.builder(),
-                ImmutableMap.builder()));
+        assertThrows(
+            IllegalStateException.class,
+            () ->
+                skydocMain.eval(
+                    Label.parseAbsoluteUnchecked("//test:main.bzl"),
+                    ruleInfoMapBuilder,
+                    ImmutableList.builder(),
+                    ImmutableMap.builder(),
+                    ImmutableMap.builder()));
 
     assertThat(expected).hasMessageThat().contains("cycle with test/main.bzl");
   }
+
+  @Test
+  public void testMalformedFunctionDocstring() throws Exception {
+    scratch.file(
+        "/test/main.bzl",
+        "def check_sources(name,",
+        "                  required_param,",
+        "                  bool_param = True,",
+        "                  srcs = []):",
+        "    \"\"\"Runs some checks on the given source files.",
+        "",
+        "    This rule runs checks on a given set of source files.",
+        "    Use `bazel build` to run the check.",
+        "",
+        "    Args:",
+        "        name: A unique name for this rule.",
+        "        required_param:",
+        "        bool_param: ..oh hey I forgot to document required_param!",
+        "    \"\"\"",
+        "    pass");
+
+    ImmutableMap.Builder<String, UserDefinedFunction> functionInfoBuilder = ImmutableMap.builder();
+
+    skydocMain.eval(
+        Label.parseAbsoluteUnchecked("//test:main.bzl"),
+        ImmutableMap.builder(),
+        ImmutableList.builder(),
+        ImmutableMap.builder(),
+        functionInfoBuilder);
+
+    UserDefinedFunction checkSourcesFn = functionInfoBuilder.build().get("check_sources");
+    DocstringParseException expected =
+        assertThrows(
+            DocstringParseException.class,
+            () -> UserDefinedFunctionInfo.fromNameAndFunction("check_sources", checkSourcesFn));
+    assertThat(expected)
+        .hasMessageThat()
+        .contains(
+            "Unable to generate documentation for function check_sources "
+                + "(defined at /test/main.bzl:1:5) due to malformed docstring. Parse errors:");
+    assertThat(expected)
+        .hasMessageThat()
+        .contains(
+            "/test/main.bzl:1:5 line 8: invalid parameter documentation "
+                + "(expected format: \"parameter_name: documentation\").");
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/config_apis_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/config_apis_test/golden.txt
index e69de29..3711458 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/config_apis_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/config_apis_test/golden.txt
@@ -0,0 +1,10 @@
+## exercise_the_api
+
+<pre>
+exercise_the_api()
+</pre>
+
+
+
+
+
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/function_basic_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/function_basic_test/golden.txt
new file mode 100644
index 0000000..ae12a73
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/function_basic_test/golden.txt
@@ -0,0 +1,122 @@
+## check_sources
+
+<pre>
+check_sources(name, required_param, bool_param, srcs, string_param, int_param, dict_param, struct_param)
+</pre>
+
+Runs some checks on the given source files.
+
+This rule runs checks on a given set of source files.
+Use `bazel build` to run the check.
+
+
+### Parameters
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr>
+      <td><code>name</code></td>
+      <td>
+        required.
+        <p>
+          A unique name for this rule.
+        </p>
+      </td>
+    </tr>
+    <tr>
+      <td><code>required_param</code></td>
+      <td>
+        required.
+        <p>
+          Use your imagination.
+        </p>
+      </td>
+    </tr>
+    <tr>
+      <td><code>bool_param</code></td>
+      <td>
+        optional. default is <code>True</code>
+      </td>
+    </tr>
+    <tr>
+      <td><code>srcs</code></td>
+      <td>
+        optional. default is <code>[]</code>
+        <p>
+          Source files to run the checks against.
+        </p>
+      </td>
+    </tr>
+    <tr>
+      <td><code>string_param</code></td>
+      <td>
+        optional. default is <code>""</code>
+      </td>
+    </tr>
+    <tr>
+      <td><code>int_param</code></td>
+      <td>
+        optional. default is <code>2</code>
+        <p>
+          Your favorite number.
+        </p>
+      </td>
+    </tr>
+    <tr>
+      <td><code>dict_param</code></td>
+      <td>
+        optional. default is <code>{}</code>
+      </td>
+    </tr>
+    <tr>
+      <td><code>struct_param</code></td>
+      <td>
+        optional.
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+
+## undocumented_function
+
+<pre>
+undocumented_function(a, b, c)
+</pre>
+
+
+
+### Parameters
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr>
+      <td><code>a</code></td>
+      <td>
+        required.
+      </td>
+    </tr>
+    <tr>
+      <td><code>b</code></td>
+      <td>
+        required.
+      </td>
+    </tr>
+    <tr>
+      <td><code>c</code></td>
+      <td>
+        required.
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/function_basic_test/input.bzl b/src/test/java/com/google/devtools/build/skydoc/testdata/function_basic_test/input.bzl
new file mode 100644
index 0000000..3755599
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/function_basic_test/input.bzl
@@ -0,0 +1,37 @@
+"""A test that verifies basic user function documentation."""
+
+def check_sources(
+        name,
+        required_param,
+        bool_param = True,
+        srcs = [],
+        string_param = "",
+        int_param = 2,
+        dict_param = {},
+        struct_param = struct(foo = "bar")):
+    """Runs some checks on the given source files.
+
+    This rule runs checks on a given set of source files.
+    Use `bazel build` to run the check.
+
+    Args:
+        name: A unique name for this rule.
+        required_param: Use your imagination.
+        srcs: Source files to run the checks against.
+        doesnt_exist: A param that doesn't exist (lets hope we still get *some* documentation)
+        int_param: Your favorite number.
+    """
+    _ignore = [
+        name,
+        required_param,
+        bool_param,
+        srcs,
+        string_param,
+        int_param,
+        dict_param,
+        struct_param,
+    ]
+    print("Hah. All that documentation but nothing really to see here")
+
+def undocumented_function(a, b, c):
+    pass
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/misc_apis_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/misc_apis_test/golden.txt
index 62c7f53..c6f2d63 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/misc_apis_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/misc_apis_test/golden.txt
@@ -101,3 +101,39 @@
 </table>
 
 
+## exercise_the_api
+
+<pre>
+exercise_the_api()
+</pre>
+
+
+
+
+
+## my_rule_impl
+
+<pre>
+my_rule_impl(ctx)
+</pre>
+
+
+
+### Parameters
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr>
+      <td><code>ctx</code></td>
+      <td>
+        required.
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/dep.bzl b/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/dep.bzl
index 129c1dd..e4f487b 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/dep.bzl
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/dep.bzl
@@ -1,5 +1,15 @@
 load(":testdata/multiple_files_test/inner_dep.bzl", "inner_rule_impl", "prep_work")
 
+def some_cool_function(name, srcs = [], beef = ""):
+    """A pretty cool function. You should call it.
+
+    Args:
+      name: Some sort of name.
+      srcs: What sources you want cool stuff to happen to.
+      beef: Your opinion on beef.
+    """
+    print(name, srcs, beef)
+
 prep_work()
 
 my_rule_impl = inner_rule_impl
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/golden.txt
index be1e046..adb6ac6 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/golden.txt
@@ -124,3 +124,114 @@
 </table>
 
 
+## my_rule_impl
+
+<pre>
+my_rule_impl(ctx)
+</pre>
+
+
+
+### Parameters
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr>
+      <td><code>ctx</code></td>
+      <td>
+        required.
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+
+## some_cool_function
+
+<pre>
+some_cool_function(name, srcs, beef)
+</pre>
+
+A pretty cool function. You should call it.
+
+### Parameters
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr>
+      <td><code>name</code></td>
+      <td>
+        required.
+        <p>
+          Some sort of name.
+        </p>
+      </td>
+    </tr>
+    <tr>
+      <td><code>srcs</code></td>
+      <td>
+        optional. default is <code>[]</code>
+        <p>
+          What sources you want cool stuff to happen to.
+        </p>
+      </td>
+    </tr>
+    <tr>
+      <td><code>beef</code></td>
+      <td>
+        optional. default is <code>""</code>
+        <p>
+          Your opinion on beef.
+        </p>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+
+## top_fun
+
+<pre>
+top_fun(a, b, c)
+</pre>
+
+
+
+### Parameters
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr>
+      <td><code>a</code></td>
+      <td>
+        required.
+      </td>
+    </tr>
+    <tr>
+      <td><code>b</code></td>
+      <td>
+        required.
+      </td>
+    </tr>
+    <tr>
+      <td><code>c</code></td>
+      <td>
+        required.
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/inner_dep.bzl b/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/inner_dep.bzl
index bb989dc..16d4361 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/inner_dep.bzl
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/inner_dep.bzl
@@ -1,4 +1,7 @@
+"""A deep dependency file."""
+
 def prep_work():
+    """Does some prep work. Nothing to see here."""
     return 1
 
 def inner_rule_impl(ctx):
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/input.bzl b/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/input.bzl
index 2c73da2..9997f43 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/input.bzl
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_files_test/input.bzl
@@ -1,4 +1,6 @@
-load(":testdata/multiple_files_test/dep.bzl", "my_rule_impl")
+"""A direct dependency file of the input file."""
+
+load(":testdata/multiple_files_test/dep.bzl", "my_rule_impl", "some_cool_function")
 
 my_rule = rule(
     implementation = my_rule_impl,
@@ -14,6 +16,10 @@
     },
 )
 
+def top_fun(a, b, c):
+    _ignore = [a, b, c]
+    return 6
+
 other_rule = rule(
     implementation = my_rule_impl,
     doc = "This is another rule.",
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_rules_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_rules_test/golden.txt
index eccbbf4..d3377cf 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_rules_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/multiple_rules_test/golden.txt
@@ -118,3 +118,29 @@
 </table>
 
 
+## my_rule_impl
+
+<pre>
+my_rule_impl(ctx)
+</pre>
+
+
+
+### Parameters
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr>
+      <td><code>ctx</code></td>
+      <td>
+        required.
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/provider_basic_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/provider_basic_test/golden.txt
index 76b3eee..743355f 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/provider_basic_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/provider_basic_test/golden.txt
@@ -1,10 +1,3 @@
-<a name="#MyPoorlyDocumentedInfo"></a>
-## MyPoorlyDocumentedInfo
-
-
-
-
-
 <a name="#MyFooInfo"></a>
 ## MyFooInfo
 
@@ -34,6 +27,13 @@
 </table>
 
 
+<a name="#MyPoorlyDocumentedInfo"></a>
+## MyPoorlyDocumentedInfo
+
+
+
+
+
 <a name="#MyVeryDocumentedInfo"></a>
 ## MyVeryDocumentedInfo
 
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/unknown_name_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/unknown_name_test/golden.txt
index 5490b82..45f77f7 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/unknown_name_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/unknown_name_test/golden.txt
@@ -52,3 +52,29 @@
 </table>
 
 
+## my_rule_impl
+
+<pre>
+my_rule_impl(ctx)
+</pre>
+
+
+
+### Parameters
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr>
+      <td><code>ctx</code></td>
+      <td>
+        required.
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+
diff --git a/src/tools/skylark/java/com/google/devtools/skylark/skylint/BUILD b/src/tools/skylark/java/com/google/devtools/skylark/skylint/BUILD
index 9bbd103..ca1dfa0 100644
--- a/src/tools/skylark/java/com/google/devtools/skylark/skylint/BUILD
+++ b/src/tools/skylark/java/com/google/devtools/skylark/skylint/BUILD
@@ -14,7 +14,11 @@
 java_library(
     name = "skylint_lib",
     srcs = glob(["**/*.java"]),
-    visibility = ["//src/tools/skylark/javatests/com/google/devtools/skylark/skylint:__pkg__"],
+    visibility = [
+        # For docstring parsing libraries.
+        "//src/main/java/com/google/devtools/build/skydoc:__subpackages__",
+        "//src/tools/skylark/javatests/com/google/devtools/skylark/skylint:__pkg__",
+    ],
     deps = [
         # TODO(bazel-team): Once BazelLibrary has a Build API interface, depend
         # on lib:skylarkbuildapi instead of on lib:packages.
diff --git a/src/tools/skylark/java/com/google/devtools/skylark/skylint/DocstringUtils.java b/src/tools/skylark/java/com/google/devtools/skylark/skylint/DocstringUtils.java
index 30457ac..ac608e2 100644
--- a/src/tools/skylark/java/com/google/devtools/skylark/skylint/DocstringUtils.java
+++ b/src/tools/skylark/java/com/google/devtools/skylark/skylint/DocstringUtils.java
@@ -119,7 +119,7 @@
 
   /** Takes a function body and returns the docstring literal, if present. */
   @Nullable
-  static StringLiteral extractDocstring(List<Statement> statements) {
+  public static StringLiteral extractDocstring(List<Statement> statements) {
     if (statements.isEmpty()) {
       return null;
     }
@@ -127,7 +127,8 @@
   }
 
   /** Parses a docstring from a string literal and appends any new errors to the given list. */
-  static DocstringInfo parseDocstring(StringLiteral docstring, List<DocstringParseError> errors) {
+  public static DocstringInfo parseDocstring(
+      StringLiteral docstring, List<DocstringParseError> errors) {
     int indentation = docstring.getLocation().getStartLineAndColumn().getColumn() - 1;
     return parseDocstring(docstring.getValue(), indentation, errors);
   }
@@ -174,7 +175,30 @@
     return result;
   }
 
-  static class DocstringInfo {
+  /** Encapsulates information about a Starlark function docstring. */
+  public static class DocstringInfo {
+
+    /** Returns the one-line summary of the docstring. */
+    public String getSummary() {
+      return summary;
+    }
+
+    /**
+     * Returns a list containing information about parameter documentation for the parameters of the
+     * documented function.
+     */
+    public List<ParameterDoc> getParameters() {
+      return parameters;
+    }
+
+    /**
+     * Returns the long-form description of the docstring. (Everything after the one-line summary
+     * and before special sections such as "Args:".
+     */
+    public String getLongDescription() {
+      return longDescription;
+    }
+
     /** The one-line summary at the start of the docstring. */
     final String summary;
     /** Documentation of function parameters from the 'Args:' section. */
@@ -208,7 +232,10 @@
     }
   }
 
-  static class ParameterDoc {
+  /**
+   * Contains information about the documentation for function parameters of a Starlark function.
+   */
+  public static class ParameterDoc {
     final String parameterName;
     final List<String> attributes; // e.g. a type annotation, "unused", "mutable"
     final String description;
@@ -218,6 +245,18 @@
       this.attributes = ImmutableList.copyOf(attributes);
       this.description = description;
     }
+
+    public String getParameterName() {
+      return parameterName;
+    }
+
+    public List<String> getAttributes() {
+      return attributes;
+    }
+
+    public String getDescription() {
+      return description;
+    }
   }
 
   private static class DocstringParser {
@@ -578,7 +617,8 @@
     }
   }
 
-  static class DocstringParseError {
+  /** Contains error information to reflect a docstring parse error. */
+  public static class DocstringParseError {
     final String message;
     final int lineNumber;
     final String line;
@@ -593,5 +633,15 @@
     public String toString() {
       return lineNumber + ": " + message;
     }
+
+    /** Returns a descriptive method about the error which occurred. */
+    public String getMessage() {
+      return message;
+    }
+
+    /** Returns the line number in the containing Starlark file which contains this error. */
+    public int getLineNumber() {
+      return lineNumber;
+    }
   }
 }