Document provider() calls in Stardoc

RELNOTES: None.
PiperOrigin-RevId: 217544224
diff --git a/src/main/java/com/google/devtools/build/docgen/SymbolFamilies.java b/src/main/java/com/google/devtools/build/docgen/SymbolFamilies.java
index f011afe..2c17a20 100644
--- a/src/main/java/com/google/devtools/build/docgen/SymbolFamilies.java
+++ b/src/main/java/com/google/devtools/build/docgen/SymbolFamilies.java
@@ -160,7 +160,7 @@
             new FakeSkylarkAttrApi(),
             new FakeSkylarkCommandLineApi(),
             new FakeSkylarkNativeModuleApi(),
-            new FakeSkylarkRuleFunctionsApi(Lists.newArrayList()),
+            new FakeSkylarkRuleFunctionsApi(Lists.newArrayList(), Lists.newArrayList()),
             new FakeStructProviderApi(),
             new FakeOutputGroupInfoProvider(),
             new FakeActionsInfoProvider(),
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 1ef470f..ebb19db 100644
--- a/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
+++ b/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
@@ -69,6 +69,7 @@
 import com.google.devtools.build.skydoc.fakebuildapi.test.FakeAnalysisTestResultInfoProvider;
 import com.google.devtools.build.skydoc.fakebuildapi.test.FakeTestingModule;
 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 java.io.IOException;
 import java.io.PrintWriter;
@@ -128,24 +129,31 @@
     ImmutableSet<String> symbolNames = getSymbolNames(args);
 
     ImmutableMap.Builder<String, RuleInfo> ruleInfoMap = ImmutableMap.builder();
+    ImmutableMap.Builder<String, ProviderInfo> providerInfoMap = ImmutableMap.builder();
     ImmutableList.Builder<RuleInfo> unknownNamedRules = ImmutableList.builder();
 
     new SkydocMain(new FilesystemFileAccessor())
-        .eval(targetFileLabel, ruleInfoMap, unknownNamedRules);
+        .eval(targetFileLabel, ruleInfoMap, unknownNamedRules, providerInfoMap);
 
     MarkdownRenderer renderer = new MarkdownRenderer();
 
     if (symbolNames.isEmpty()) {
       try (PrintWriter printWriter = new PrintWriter(outputPath, "UTF-8")) {
         printRuleInfos(printWriter, renderer, ruleInfoMap.build(), unknownNamedRules.build());
+        printProviderInfos(printWriter, renderer, providerInfoMap.build());
       }
     } else {
-      Map<String, RuleInfo> filteredRuleInfos = ImmutableMap.copyOf(
+      Map<String, RuleInfo> filteredRuleInfos =
           ruleInfoMap.build().entrySet().stream()
               .filter(entry -> symbolNames.contains(entry.getKey()))
-              .collect(Collectors.toList()));
+              .collect(ImmutableMap.toImmutableMap(Entry::getKey, Entry::getValue));
+      Map<String, ProviderInfo> filteredProviderInfos =
+          providerInfoMap.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);
       }
     }
   }
@@ -173,38 +181,64 @@
     }
   }
 
+  private static void printProviderInfos(
+      PrintWriter printWriter,
+      MarkdownRenderer renderer,
+      Map<String, ProviderInfo> providerInfos) throws IOException {
+    for (Entry<String, ProviderInfo> entry : providerInfos.entrySet()) {
+      printProviderInfo(printWriter, renderer, entry.getKey(), entry.getValue());
+      printWriter.println();
+    }
+  }
+
   private static void printRuleInfo(
       PrintWriter printWriter, MarkdownRenderer renderer,
       String exportedName, RuleInfo ruleInfo) throws IOException {
     printWriter.println(renderer.render(exportedName, ruleInfo));
   }
 
+  private static void printProviderInfo(
+      PrintWriter printWriter, MarkdownRenderer renderer,
+      String exportedName, ProviderInfo providerInfo) throws IOException {
+    printWriter.println(renderer.render(exportedName, providerInfo));
+  }
+
   /**
    * 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 skylark file.
+   * using a fake build API and collects information about all rule definitions made in the root
+   * skylark file.
    *
    * @param label the label of the skylark file to evaluate
-   * @param ruleInfoMap a map builder to be populated with rule definition information for
-   *     named rules. Keys are exported names of rules, and values are their {@link RuleInfo}
-   *     rule descriptions. For example, 'my_rule = rule(...)' has key 'my_rule'
-   * @param unknownNamedRules a list builder to be populated with rule definition information
-   *     for rules which were not exported as top level symbols
+   * @param ruleInfoMap a map builder to be populated with rule definition information for named
+   *     rules. Keys are exported names of rules, and values are their {@link RuleInfo} rule
+   *     descriptions. For example, 'my_rule = rule(...)' has key 'my_rule'
+   * @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
+   *     'my_provider'
    * @throws InterruptedException if evaluation is interrupted
    */
   public Environment eval(
       Label label,
       ImmutableMap.Builder<String, RuleInfo> ruleInfoMap,
-      ImmutableList.Builder<RuleInfo> unknownNamedRules)
+      ImmutableList.Builder<RuleInfo> unknownNamedRules,
+      ImmutableMap.Builder<String, ProviderInfo> providerInfoMap)
       throws InterruptedException, IOException, LabelSyntaxException {
 
     List<RuleInfo> ruleInfoList = new ArrayList<>();
-    Environment env = recursiveEval(label, ruleInfoList);
+    List<ProviderInfo> providerInfoList = new ArrayList<>();
+    Environment env = recursiveEval(label, ruleInfoList, providerInfoList);
 
     Map<BaseFunction, RuleInfo> ruleFunctions = ruleInfoList.stream()
         .collect(Collectors.toMap(
             RuleInfo::getIdentifierFunction,
             Functions.identity()));
+    Map<BaseFunction, ProviderInfo> providerInfos = providerInfoList.stream()
+        .collect(Collectors.toMap(
+            ProviderInfo::getIdentifier,
+            Functions.identity()));
 
     ImmutableSet.Builder<RuleInfo> handledRuleDefinitions = ImmutableSet.builder();
     for (Entry<String, Object> envEntry : env.getGlobals().getBindings().entrySet()) {
@@ -213,6 +247,10 @@
         ruleInfoMap.put(envEntry.getKey(), ruleInfo);
         handledRuleDefinitions.add(ruleInfo);
       }
+      if (providerInfos.containsKey(envEntry.getValue())) {
+        ProviderInfo providerInfo = providerInfos.get(envEntry.getValue());
+        providerInfoMap.put(envEntry.getKey(), providerInfo);
+      }
     }
 
     unknownNamedRules.addAll(ruleFunctions.values().stream()
@@ -232,7 +270,7 @@
    * @throws InterruptedException if evaluation is interrupted
    */
   private Environment recursiveEval(
-      Label label, List<RuleInfo> ruleInfoList)
+      Label label, List<RuleInfo> ruleInfoList, List<ProviderInfo> providerInfoList)
       throws InterruptedException, IOException, LabelSyntaxException {
     Path path = pathOfLabel(label);
 
@@ -251,7 +289,7 @@
       Label relativeLabel = label.getRelative(anImport.getImportString());
 
       try {
-        Environment importEnv = recursiveEval(relativeLabel, ruleInfoList);
+        Environment importEnv = recursiveEval(relativeLabel, ruleInfoList, providerInfoList);
         imports.put(anImport.getImportString(), new Extension(importEnv));
       } catch (NoSuchFileException noSuchFileException) {
         throw new IllegalStateException(
@@ -260,7 +298,7 @@
       }
     }
 
-    Environment env = evalSkylarkBody(buildFileAST, imports, ruleInfoList);
+    Environment env = evalSkylarkBody(buildFileAST, imports, ruleInfoList, providerInfoList);
 
     pending.remove(path);
     env.mutability().freeze();
@@ -282,11 +320,12 @@
   private Environment evalSkylarkBody(
       BuildFileAST buildFileAST,
       Map<String, Extension> imports,
-      List<RuleInfo> ruleInfoList) throws InterruptedException {
+      List<RuleInfo> ruleInfoList,
+      List<ProviderInfo> providerInfoList) throws InterruptedException {
 
     Environment env = createEnvironment(
         eventHandler,
-        globalFrame(ruleInfoList),
+        globalFrame(ruleInfoList, providerInfoList),
         imports);
 
     if (!buildFileAST.exec(env, eventHandler)) {
@@ -303,14 +342,17 @@
    *
    * @param ruleInfoList the list of {@link RuleInfo} objects, to which rule() invocation
    *     information will be added
+   * @param providerInfoList the list of {@link ProviderInfo} objects, to which provider()
+   *     invocation information will be added
    */
-  private static GlobalFrame globalFrame(List<RuleInfo> ruleInfoList) {
+  private static GlobalFrame globalFrame(List<RuleInfo> ruleInfoList,
+      List<ProviderInfo> providerInfoList) {
     TopLevelBootstrap topLevelBootstrap =
         new TopLevelBootstrap(new FakeBuildApiGlobals(),
             new FakeSkylarkAttrApi(),
             new FakeSkylarkCommandLineApi(),
             new FakeSkylarkNativeModuleApi(),
-            new FakeSkylarkRuleFunctionsApi(ruleInfoList),
+            new FakeSkylarkRuleFunctionsApi(ruleInfoList, providerInfoList),
             new FakeStructProviderApi(),
             new FakeOutputGroupInfoProvider(),
             new FakeActionsInfoProvider(),
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeProviderApi.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeProviderApi.java
index 01b64c1..645f96e 100644
--- a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeProviderApi.java
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeProviderApi.java
@@ -29,8 +29,14 @@
  */
 public class FakeProviderApi extends BaseFunction implements ProviderApi {
 
+  /**
+   * Each fake is constructed with a unique name, controlled by this counter being the name suffix.
+   */
+  private static int idCounter = 0;
+
   public FakeProviderApi() {
-    super("ProviderFunction", FunctionSignature.WithValues.create(FunctionSignature.KWARGS));
+    super("ProviderIdentifier" + idCounter++,
+        FunctionSignature.WithValues.create(FunctionSignature.KWARGS));
   }
 
   @Override
@@ -41,4 +47,10 @@
 
   @Override
   public void repr(SkylarkPrinter printer) {}
+
+  @Override
+  public boolean equals(@Nullable Object other) {
+    // Use exact object matching.
+    return this == other;
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkRuleFunctionsApi.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkRuleFunctionsApi.java
index 8cd428e..1c3d82b 100644
--- a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkRuleFunctionsApi.java
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkRuleFunctionsApi.java
@@ -14,6 +14,7 @@
 
 package com.google.devtools.build.skydoc.fakebuildapi;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.cmdline.Label;
 import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
@@ -30,11 +31,15 @@
 import com.google.devtools.build.lib.syntax.Runtime;
 import com.google.devtools.build.lib.syntax.SkylarkDict;
 import com.google.devtools.build.lib.syntax.SkylarkList;
+import com.google.devtools.build.lib.syntax.SkylarkType;
 import com.google.devtools.build.skydoc.fakebuildapi.FakeDescriptor.Type;
 import com.google.devtools.build.skydoc.rendering.AttributeInfo;
+import com.google.devtools.build.skydoc.rendering.ProviderFieldInfo;
+import com.google.devtools.build.skydoc.rendering.ProviderInfo;
 import com.google.devtools.build.skydoc.rendering.RuleInfo;
 import java.util.Comparator;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 
@@ -49,20 +54,50 @@
   private static final FakeDescriptor IMPLICIT_NAME_ATTRIBUTE_DESCRIPTOR =
       new FakeDescriptor(Type.STRING, "A unique name for this target.", true);
   private final List<RuleInfo> ruleInfoList;
+  private final List<ProviderInfo> providerInfoList;
 
   /**
    * Constructor.
    *
    * @param ruleInfoList the list of {@link RuleInfo} objects to which rule() invocation information
    *     will be added
+   * @param providerInfoList the list of {@link ProviderInfo} objects to which provider()
+   *     invocation information will be added
    */
-  public FakeSkylarkRuleFunctionsApi(List<RuleInfo> ruleInfoList) {
+  public FakeSkylarkRuleFunctionsApi(List<RuleInfo> ruleInfoList,
+      List<ProviderInfo> providerInfoList) {
     this.ruleInfoList = ruleInfoList;
+    this.providerInfoList = providerInfoList;
   }
 
   @Override
   public ProviderApi provider(String doc, Object fields, Location location) throws EvalException {
-    return new FakeProviderApi();
+    FakeProviderApi fakeProvider = new FakeProviderApi();
+    // Field documentation will be output preserving the order in which the fields are listed.
+    ImmutableList.Builder<ProviderFieldInfo> providerFieldInfos = ImmutableList.builder();
+    if (fields instanceof SkylarkList) {
+      @SuppressWarnings("unchecked")
+      SkylarkList<String> fieldNames = (SkylarkList<String>)
+          SkylarkType.cast(
+              fields,
+              SkylarkList.class, String.class, location,
+              "Expected list of strings or dictionary of string -> string for 'fields'");
+      for (String fieldName : fieldNames) {
+        providerFieldInfos.add(new ProviderFieldInfo(fieldName));
+      }
+    } else if (fields instanceof SkylarkDict) {
+      Map<String, String> dict = SkylarkType.castMap(
+          fields,
+          String.class, String.class,
+          "Expected list of strings or dictionary of string -> string for 'fields'");
+      for (Map.Entry<String, String> fieldEntry : dict.entrySet()) {
+        providerFieldInfos.add(new ProviderFieldInfo(fieldEntry.getKey(), fieldEntry.getValue()));
+      }
+    } else {
+      // fields is NONE, so there is no field information to add.
+    }
+    providerInfoList.add(new ProviderInfo(fakeProvider, doc, providerFieldInfos.build()));
+    return fakeProvider;
   }
 
   @Override
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 9632d53..9c82cde 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
@@ -32,8 +32,10 @@
  */
 public class MarkdownRenderer {
 
-  private static final String TEMPLATE_FILENAME =
+  private static final String RULE_TEMPLATE_FILENAME =
       "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 final VelocityEngine velocityEngine;
 
@@ -61,7 +63,25 @@
 
     StringWriter stringWriter = new StringWriter();
     try {
-      velocityEngine.mergeTemplate(TEMPLATE_FILENAME, "UTF-8", context, stringWriter);
+      velocityEngine.mergeTemplate(RULE_TEMPLATE_FILENAME, "UTF-8", context, stringWriter);
+    } catch (ResourceNotFoundException | ParseErrorException | MethodInvocationException e) {
+      throw new IOException(e);
+    }
+    return stringWriter.toString();
+  }
+
+  /**
+   * Returns a markdown rendering of provider documentation for the given provider information
+   * object with the given name.
+   */
+  public String render(String providerName, ProviderInfo providerInfo) throws IOException {
+    VelocityContext context = new VelocityContext();
+    context.put("providerName", providerName);
+    context.put("providerInfo", providerInfo);
+
+    StringWriter stringWriter = new StringWriter();
+    try {
+      velocityEngine.mergeTemplate(PROVIDER_TEMPLATE_FILENAME, "UTF-8", context, stringWriter);
     } catch (ResourceNotFoundException | ParseErrorException | MethodInvocationException e) {
       throw new IOException(e);
     }
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/ProviderFieldInfo.java b/src/main/java/com/google/devtools/build/skydoc/rendering/ProviderFieldInfo.java
new file mode 100644
index 0000000..4697622
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/ProviderFieldInfo.java
@@ -0,0 +1,42 @@
+// 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;
+
+/**
+ * Stores information about a provider field definition.
+ */
+public class ProviderFieldInfo {
+  private final String name;
+  private final String docString;
+
+  public ProviderFieldInfo(String name) {
+    this(name, "(Undocumented)");
+  }
+
+  public ProviderFieldInfo(String name, String docString) {
+    this.name = name;
+    this.docString = docString.trim();
+  }
+
+  @SuppressWarnings("unused") // Used by markdown template.
+  public String getName() {
+    return name;
+  }
+
+  @SuppressWarnings("unused") // Used by markdown template.
+  public String getDocString() {
+    return docString;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/ProviderInfo.java b/src/main/java/com/google/devtools/build/skydoc/rendering/ProviderInfo.java
new file mode 100644
index 0000000..3c39a11
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/ProviderInfo.java
@@ -0,0 +1,52 @@
+// 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.BaseFunction;
+import java.util.Collection;
+
+/**
+ * Stores information about a starlark provider definition.
+ *
+ * For example, in <pre>FooInfo = provider(doc = 'My provider', fields = {'bar' : 'a bar'})</pre>,
+ * this contains all information about the definition of FooInfo for purposes of generating its
+ * documentation.
+ */
+public class ProviderInfo {
+
+  private final BaseFunction identifier;
+  private final String docString;
+  private final Collection<ProviderFieldInfo> fieldInfos;
+
+  public ProviderInfo(BaseFunction identifier,
+      String docString,
+      Collection<ProviderFieldInfo> fieldInfos) {
+    this.identifier = identifier;
+    this.docString = docString;
+    this.fieldInfos = fieldInfos;
+  }
+
+  public BaseFunction getIdentifier() {
+    return identifier;
+  }
+
+  public String getDocString() {
+    return docString;
+  }
+
+  public Collection<ProviderFieldInfo> getFields() {
+    return fieldInfos;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/templates/provider.vm b/src/main/java/com/google/devtools/build/skydoc/rendering/templates/provider.vm
new file mode 100644
index 0000000..f708daa
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/templates/provider.vm
@@ -0,0 +1,25 @@
+<a name="#${providerName}"></a>
+#[[##]]# ${providerName}
+
+${providerInfo.docString}
+
+#if (!$providerInfo.fields.isEmpty())
+#[[###]]# Fields
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+#foreach ($field in $providerInfo.fields)
+    <tr id="#${providerName}_${field.name}">
+      <td><code>${field.name}</code></td>
+      <td>
+        <p>${field.docString}</p>
+      </td>
+    </tr>
+#end
+  </tbody>
+</table>
+#end