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;
+ }
}
}