Allows Stardoc to document the providers a rule attribute requires

PiperOrigin-RevId: 262222091
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeDescriptor.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeDescriptor.java
index 1793dad..30668a4 100644
--- a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeDescriptor.java
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeDescriptor.java
@@ -18,6 +18,8 @@
 import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AttributeInfo;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AttributeType;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ProviderNameGroup;
+import java.util.List;
 
 /**
  * Fake implementation of {@link Descriptor}.
@@ -26,34 +28,38 @@
   private final AttributeType type;
   private final String docString;
   private final boolean mandatory;
+  private final List<List<String>> providerNameGroups;
 
-  public FakeDescriptor(AttributeType type, String docString, boolean mandatory) {
+  public FakeDescriptor(
+      AttributeType type,
+      String docString,
+      boolean mandatory,
+      List<List<String>> providerNameGroups) {
     this.type = type;
     this.docString = docString;
     this.mandatory = mandatory;
-  }
-
-  public AttributeType getType() {
-    return type;
-  }
-
-  public String getDocString() {
-    return docString;
-  }
-
-  public boolean isMandatory() {
-    return mandatory;
+    this.providerNameGroups = providerNameGroups;
   }
 
   @Override
   public void repr(SkylarkPrinter printer) {}
 
   public AttributeInfo asAttributeInfo(String attributeName) {
-    return AttributeInfo.newBuilder()
-        .setName(attributeName)
-        .setDocString(getDocString())
-        .setType(getType())
-        .setMandatory(isMandatory())
-        .build();
+    AttributeInfo.Builder attrInfo =
+        AttributeInfo.newBuilder()
+            .setName(attributeName)
+            .setDocString(docString)
+            .setType(type)
+            .setMandatory(mandatory);
+
+    if (!providerNameGroups.isEmpty()) {
+      for (List<String> providerNameGroup : providerNameGroups) {
+        ProviderNameGroup.Builder providerNameListBuild = ProviderNameGroup.newBuilder();
+        ProviderNameGroup providerNameList =
+            providerNameListBuild.addAllProviderName(providerNameGroup).build();
+        attrInfo.addProviderNameGroup(providerNameList);
+      }
+    }
+    return attrInfo.build();
   }
 }
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkAttrApi.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkAttrApi.java
index a67fff7..5935b0e 100644
--- a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkAttrApi.java
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkAttrApi.java
@@ -14,6 +14,8 @@
 
 package com.google.devtools.build.skydoc.fakebuildapi;
 
+import com.google.common.collect.ImmutableList;
+import com.google.devtools.build.lib.skylarkbuildapi.ProviderApi;
 import com.google.devtools.build.lib.skylarkbuildapi.SkylarkAttrApi;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
 import com.google.devtools.build.lib.skylarkinterface.StarlarkContext;
@@ -23,6 +25,10 @@
 import com.google.devtools.build.lib.syntax.SkylarkDict;
 import com.google.devtools.build.lib.syntax.SkylarkList;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AttributeType;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 
 /**
  * Fake implementation of {@link SkylarkAttrApi}.
@@ -39,7 +45,7 @@
       Environment env,
       StarlarkContext context)
       throws EvalException {
-    return new FakeDescriptor(AttributeType.INT, doc, mandatory);
+    return new FakeDescriptor(AttributeType.INT, doc, mandatory, ImmutableList.of());
   }
 
   @Override
@@ -52,7 +58,7 @@
       Environment env,
       StarlarkContext context)
       throws EvalException {
-    return new FakeDescriptor(AttributeType.STRING, doc, mandatory);
+    return new FakeDescriptor(AttributeType.STRING, doc, mandatory, ImmutableList.of());
   }
 
   @Override
@@ -72,7 +78,11 @@
       Environment env,
       StarlarkContext context)
       throws EvalException {
-    return new FakeDescriptor(AttributeType.LABEL, doc, mandatory);
+    List<List<String>> allNameGroups = new ArrayList<>();
+    if (providers != null) {
+      allNameGroups = allProviderNameGroups(providers, env);
+    }
+    return new FakeDescriptor(AttributeType.LABEL, doc, mandatory, allNameGroups);
   }
 
   @Override
@@ -86,7 +96,7 @@
       Environment env,
       StarlarkContext context)
       throws EvalException {
-    return new FakeDescriptor(AttributeType.STRING_LIST, doc, mandatory);
+    return new FakeDescriptor(AttributeType.STRING_LIST, doc, mandatory, ImmutableList.of());
   }
 
   @Override
@@ -100,7 +110,7 @@
       Environment env,
       StarlarkContext context)
       throws EvalException {
-    return new FakeDescriptor(AttributeType.INT_LIST, doc, mandatory);
+    return new FakeDescriptor(AttributeType.INT_LIST, doc, mandatory, ImmutableList.of());
   }
 
   @Override
@@ -120,7 +130,11 @@
       Environment env,
       StarlarkContext context)
       throws EvalException {
-    return new FakeDescriptor(AttributeType.LABEL_LIST, doc, mandatory);
+    List<List<String>> allNameGroups = new ArrayList<>();
+    if (providers != null) {
+      allNameGroups = allProviderNameGroups(providers, env);
+    }
+    return new FakeDescriptor(AttributeType.LABEL_LIST, doc, mandatory, allNameGroups);
   }
 
   @Override
@@ -140,7 +154,11 @@
       Environment env,
       StarlarkContext context)
       throws EvalException {
-    return new FakeDescriptor(AttributeType.LABEL_STRING_DICT, doc, mandatory);
+    List<List<String>> allNameGroups = new ArrayList<>();
+    if (providers != null) {
+      allNameGroups = allProviderNameGroups(providers, env);
+    }
+    return new FakeDescriptor(AttributeType.LABEL_STRING_DICT, doc, mandatory, allNameGroups);
   }
 
   @Override
@@ -152,7 +170,7 @@
       Environment env,
       StarlarkContext context)
       throws EvalException {
-    return new FakeDescriptor(AttributeType.BOOLEAN, doc, mandatory);
+    return new FakeDescriptor(AttributeType.BOOLEAN, doc, mandatory, ImmutableList.of());
   }
 
   @Override
@@ -164,7 +182,7 @@
       Environment env,
       StarlarkContext context)
       throws EvalException {
-    return new FakeDescriptor(AttributeType.OUTPUT, doc, mandatory);
+    return new FakeDescriptor(AttributeType.OUTPUT, doc, mandatory, ImmutableList.of());
   }
 
   @Override
@@ -178,7 +196,7 @@
       Environment env,
       StarlarkContext context)
       throws EvalException {
-    return new FakeDescriptor(AttributeType.OUTPUT_LIST, doc, mandatory);
+    return new FakeDescriptor(AttributeType.OUTPUT_LIST, doc, mandatory, ImmutableList.of());
   }
 
   @Override
@@ -192,7 +210,7 @@
       Environment env,
       StarlarkContext context)
       throws EvalException {
-    return new FakeDescriptor(AttributeType.STRING_DICT, doc, mandatory);
+    return new FakeDescriptor(AttributeType.STRING_DICT, doc, mandatory, ImmutableList.of());
   }
 
   @Override
@@ -206,7 +224,7 @@
       Environment env,
       StarlarkContext context)
       throws EvalException {
-    return new FakeDescriptor(AttributeType.STRING_LIST_DICT, doc, mandatory);
+    return new FakeDescriptor(AttributeType.STRING_LIST_DICT, doc, mandatory, ImmutableList.of());
   }
 
   @Override
@@ -218,9 +236,72 @@
       Environment env,
       StarlarkContext context)
       throws EvalException {
-    return new FakeDescriptor(AttributeType.STRING_LIST, doc, mandatory);
+    return new FakeDescriptor(AttributeType.STRING_LIST, doc, mandatory, ImmutableList.of());
   }
 
   @Override
   public void repr(SkylarkPrinter printer) {}
+
+  /**
+   * Returns a list of provider name groups, given the value of a Starlark attribute's "providers"
+   * argument.
+   *
+   * <p>{@code providers} can either be a list of providers, or a list of lists of providers. If it
+   * is the first case, the entire list is considered a single group. In the second case, each of
+   * the inner lists is a group.
+   */
+  private static List<List<String>> allProviderNameGroups(SkylarkList<?> providers, Environment env)
+      throws EvalException {
+
+    List<List<String>> allNameGroups = new ArrayList<>();
+    List<List<ProviderApi>> allProviderGroups = new ArrayList<>();
+    for (Object object : providers) {
+      if (object instanceof ProviderApi) {
+        allProviderGroups.add(providers.getContents(ProviderApi.class, "providers"));
+        break;
+      } else if (object instanceof SkylarkList) {
+        allProviderGroups.add(
+            ((SkylarkList<?>) object).getContents(ProviderApi.class, "provider groups"));
+      }
+    }
+
+    for (List<ProviderApi> providerGroup : allProviderGroups) {
+      List<String> nameGroup = providerNameGroup(providerGroup, env);
+      allNameGroups.add(nameGroup);
+    }
+
+    return allNameGroups;
+  }
+
+  /** Returns the names of the providers in the given group. */
+  private static List<String> providerNameGroup(List<ProviderApi> providerGroup, Environment env) {
+    List<String> providerNameGroup = new ArrayList<>();
+    for (ProviderApi provider : providerGroup) {
+      String providerName = providerName(provider, env);
+      providerNameGroup.add(providerName);
+    }
+    return providerNameGroup;
+  }
+
+  /**
+   * Returns the name of {@code provider}.
+   *
+   * <p>{@code env} contains a {@code Map<String, Object>} where the values are built-in objects or
+   * objects defined in the file and the keys are the names of these objects. If a {@code provider}
+   * is in the map, the name of the provider is set as the key of this object in {@code bindings}.
+   * If it is not in the map, the provider may be part of a module in the map and the name will be
+   * set to "Unknown Provider".
+   */
+  private static String providerName(ProviderApi provider, Environment env) {
+    Map<String, Object> bindings = env.getGlobals().getTransitiveBindings();
+    if (bindings.containsValue(provider)) {
+      for (Entry<String, Object> envEntry : bindings.entrySet()) {
+        if (provider.equals(envEntry.getValue())) {
+          return envEntry.getKey();
+        }
+      }
+    }
+    return "Unknown Provider";
+  }
 }
+
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 c30ab1e..4eb7f01 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
@@ -58,7 +58,8 @@
 public class FakeSkylarkRuleFunctionsApi implements SkylarkRuleFunctionsApi<FileApi> {
 
   private static final FakeDescriptor IMPLICIT_NAME_ATTRIBUTE_DESCRIPTOR =
-      new FakeDescriptor(AttributeType.NAME, "A unique name for this target.", true);
+      new FakeDescriptor(
+          AttributeType.NAME, "A unique name for this target.", true, ImmutableList.of());
   private final List<RuleInfoWrapper> ruleInfoList;
 
   private final List<ProviderInfoWrapper> providerInfoList;
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/repository/FakeRepositoryModule.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/repository/FakeRepositoryModule.java
index 7c1ab1c..f21a7aa 100644
--- a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/repository/FakeRepositoryModule.java
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/repository/FakeRepositoryModule.java
@@ -14,6 +14,7 @@
 
 package com.google.devtools.build.skydoc.fakebuildapi.repository;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.devtools.build.lib.skylarkbuildapi.repository.RepositoryModuleApi;
 import com.google.devtools.build.lib.syntax.BaseFunction;
@@ -38,7 +39,8 @@
  */
 public class FakeRepositoryModule implements RepositoryModuleApi {
   private static final FakeDescriptor IMPLICIT_NAME_ATTRIBUTE_DESCRIPTOR =
-      new FakeDescriptor(AttributeType.NAME, "A unique name for this repository.", true);
+      new FakeDescriptor(
+          AttributeType.NAME, "A unique name for this repository.", true, ImmutableList.of());
 
   private final List<RuleInfoWrapper> ruleInfoList;
 
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownUtil.java b/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownUtil.java
index 44e6e45..3392e70 100644
--- a/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownUtil.java
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/MarkdownUtil.java
@@ -20,8 +20,10 @@
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AttributeType;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.FunctionParamInfo;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ProviderInfo;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ProviderNameGroup;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.RuleInfo;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.UserDefinedFunctionInfo;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -150,6 +152,19 @@
     return paramInfo.getMandatory() ? "required" : "optional";
   }
 
+  /**
+   * Return a string explaining what providers an attribute requires. Adds hyperlinks to providers.
+   */
+  public String attributeProviders(AttributeInfo attributeInfo) {
+    List<ProviderNameGroup> providerNames = attributeInfo.getProviderNameGroupList();
+    List<String> finalProviderNames = new ArrayList<>();
+    for (ProviderNameGroup providerNameList : providerNames) {
+      List<String> providers = providerNameList.getProviderNameList();
+      finalProviderNames.add(String.format(Joiner.on(", ").join(providers)));
+    }
+    return String.format(Joiner.on("; or ").join(finalProviderNames));
+  }
+
   private String attributeTypeDescription(AttributeType attributeType) {
     switch (attributeType) {
       case NAME:
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/proto/stardoc_output.proto b/src/main/java/com/google/devtools/build/skydoc/rendering/proto/stardoc_output.proto
index d8850bf..38c1d8d 100644
--- a/src/main/java/com/google/devtools/build/skydoc/rendering/proto/stardoc_output.proto
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/proto/stardoc_output.proto
@@ -88,6 +88,22 @@
 
   // If true, all targets of the rule must specify a value for this attribute.
   bool mandatory = 4;
+
+  // The target(s) in this attribute must define all the providers of at least
+  // one of the ProviderNameGroups in this list. If the Attribute Type is not a
+  // label, a label list, or a label-keyed string dictionary, the field will be
+  // left empty.
+  repeated ProviderNameGroup provider_name_group = 5;
+}
+
+// Representation of a set of providers that a rule attribute may be required to
+// have.
+message ProviderNameGroup {
+  // The names of the providers that must be given by any dependency appearing
+  // in this attribute. The name will be "Unknown Provider" if the name is
+  // unidentifiable, for example, if the provider is part of a namespace.
+  // TODO(kendalllane): Fix documentation of providers from namespaces.
+  repeated string provider_name = 1;
 }
 
 // Representation of Starlark function definition.
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 29ca936..92eec57 100644
--- a/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java
+++ b/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java
@@ -547,4 +547,53 @@
         .inOrder();
     assertThat(aspectInfo.getAspectAttributeList()).containsExactly("deps");
   }
+
+  @Test
+  public void testProvidersForAttributes() throws Exception {
+    scratch.file(
+        "/test/test.bzl",
+        "\n"
+            + "def my_rule_impl(ctx):\n"
+            + "    return struct()\n"
+            + "MyInfo = provider()\n"
+            + "my_rule = rule(\n"
+            + "    implementation = my_rule_impl,\n"
+            + "    attrs = {\n"
+            + "        \"deps\": attr.label_list(\n"
+            + "            doc = \"\"\"\n"
+            + "                   A list of dependencies.\n"
+            + "                   \"\"\",\n"
+            + "            providers = [MyInfo],\n"
+            + "            allow_files = False,"
+            + "          )"
+            + "    },"
+            + ")\n");
+
+    ImmutableMap.Builder<String, RuleInfo> ruleInfoMap = ImmutableMap.builder();
+    ImmutableMap.Builder<String, ProviderInfo> providerInfoMap = ImmutableMap.builder();
+
+    skydocMain.eval(
+        StarlarkSemantics.DEFAULT_SEMANTICS,
+        Label.parseAbsoluteUnchecked("//test:test.bzl"),
+        ruleInfoMap,
+        providerInfoMap,
+        ImmutableMap.builder(),
+        ImmutableMap.builder());
+
+    Map<String, RuleInfo> rules = ruleInfoMap.build();
+    Map<String, ProviderInfo> providers = providerInfoMap.build();
+
+    ModuleInfo moduleInfo =
+        new ProtoRenderer()
+            .appendRuleInfos(rules.values())
+            .appendProviderInfos(providers.values())
+            .getModuleInfo()
+            .build();
+    RuleInfo ruleInfo = moduleInfo.getRuleInfo(0);
+    ProviderInfo providerInfo = moduleInfo.getProviderInfo(0);
+
+    assertThat(getAttrNames(ruleInfo)).containsExactly("name", "deps").inOrder();
+    assertThat(ruleInfo.getAttribute(1).getProviderNameGroupList()).hasSize(1);
+    assertThat(providerInfo.getProviderName()).isEqualTo("MyInfo");
+  }
 }