Add attribute information to skydoc output

RELNOTES: None.
PiperOrigin-RevId: 204147228
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 249ee60..3dc7a43 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
@@ -21,19 +21,60 @@
  * Fake implementation of {@link Descriptor}.
  */
 public class FakeDescriptor implements Descriptor {
+  private final Type type;
   private final String docString;
+  private final boolean mandatory;
 
-  public FakeDescriptor(String docString) {
+  public FakeDescriptor(Type type, String docString, boolean mandatory) {
+    this.type = type;
     this.docString = docString;
+    this.mandatory = mandatory;
+  }
+
+  public Type getType() {
+    return type;
   }
 
   public String getDocString() {
     return docString;
   }
 
+  public boolean isMandatory() {
+    return mandatory;
+  }
+
   @Override
   public void repr(SkylarkPrinter printer) {}
 
-  // TODO(cparsons): This class should store information about the attribute definition, for
-  // example, the attribute type.
+  /**
+   * Attribute type. For example, an attribute described by attr.label() will be of type LABEL.
+   */
+  public enum Type {
+    INT("Integer"),
+    LABEL("Label"),
+    STRING("String"),
+    STRING_LIST("List of strings"),
+    INT_LIST("List of integers"),
+    LABEL_LIST("List of labels"),
+    BOOLEAN("Boolean"),
+    LICENSE("List of strings"),
+    LABEL_STRING_DICT("Dictionary: Label -> String"),
+    STRING_DICT("Dictionary: String -> String"),
+    STRING_LIST_DICT("Dictionary: String -> List of strings"),
+    OUTPUT("Label"),
+    OUTPUT_LIST("List of labels");
+
+    private final String description;
+
+    Type(String description) {
+      this.description = description;
+    }
+
+    /**
+     * Returns a human-readable string representing this attribute type.
+     */
+    public String getDescription() {
+      return description;
+    }
+  }
 }
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 3dcfaa2..f0ea8a6 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
@@ -21,6 +21,7 @@
 import com.google.devtools.build.lib.syntax.FuncallExpression;
 import com.google.devtools.build.lib.syntax.SkylarkDict;
 import com.google.devtools.build.lib.syntax.SkylarkList;
+import com.google.devtools.build.skydoc.fakebuildapi.FakeDescriptor.Type;
 
 /**
  * Fake implementation of {@link SkylarkAttrApi}.
@@ -30,13 +31,13 @@
   @Override
   public Descriptor intAttribute(Integer defaultInt, String doc, Boolean mandatory,
       SkylarkList<?> values, FuncallExpression ast, Environment env) throws EvalException {
-    return new FakeDescriptor(doc);
+    return new FakeDescriptor(Type.INT, doc, mandatory);
   }
 
   @Override
   public Descriptor stringAttribute(String defaultString, String doc, Boolean mandatory,
       SkylarkList<?> values, FuncallExpression ast, Environment env) throws EvalException {
-    return new FakeDescriptor(doc);
+    return new FakeDescriptor(Type.STRING, doc, mandatory);
   }
 
   @Override
@@ -44,21 +45,21 @@
       Object allowFiles, Object allowSingleFile, Boolean mandatory, SkylarkList<?> providers,
       Object allowRules, Boolean singleFile, Object cfg, SkylarkList<?> aspects,
       FuncallExpression ast, Environment env) throws EvalException {
-    return new FakeDescriptor(doc);
+    return new FakeDescriptor(Type.LABEL, doc, mandatory);
   }
 
   @Override
   public Descriptor stringListAttribute(Boolean mandatory, Boolean nonEmpty, Boolean allowEmpty,
       SkylarkList<?> defaultList, String doc, FuncallExpression ast, Environment env)
       throws EvalException {
-    return new FakeDescriptor(doc);
+    return new FakeDescriptor(Type.STRING_LIST, doc, mandatory);
   }
 
   @Override
   public Descriptor intListAttribute(Boolean mandatory, Boolean nonEmpty, Boolean allowEmpty,
       SkylarkList<?> defaultList, String doc, FuncallExpression ast, Environment env)
       throws EvalException {
-    return new FakeDescriptor(doc);
+    return new FakeDescriptor(Type.INT_LIST, doc, mandatory);
   }
 
   @Override
@@ -66,7 +67,7 @@
       Object allowFiles, Object allowRules, SkylarkList<?> providers, SkylarkList<?> flags,
       Boolean mandatory, Boolean nonEmpty, Object cfg, SkylarkList<?> aspects,
       FuncallExpression ast, Environment env) throws EvalException {
-    return new FakeDescriptor(doc);
+    return new FakeDescriptor(Type.LABEL_LIST, doc, mandatory);
   }
 
   @Override
@@ -74,46 +75,46 @@
       String doc, Object allowFiles, Object allowRules, SkylarkList<?> providers,
       SkylarkList<?> flags, Boolean mandatory, Boolean nonEmpty, Object cfg, SkylarkList<?> aspects,
       FuncallExpression ast, Environment env) throws EvalException {
-    return new FakeDescriptor(doc);
+    return new FakeDescriptor(Type.LABEL_STRING_DICT, doc, mandatory);
   }
 
   @Override
   public Descriptor boolAttribute(Boolean defaultO, String doc, Boolean mandatory,
       FuncallExpression ast, Environment env) throws EvalException {
-    return new FakeDescriptor(doc);
+    return new FakeDescriptor(Type.BOOLEAN, doc, mandatory);
   }
 
   @Override
   public Descriptor outputAttribute(Object defaultO, String doc, Boolean mandatory,
       FuncallExpression ast, Environment env) throws EvalException {
-    return new FakeDescriptor(doc);
+    return new FakeDescriptor(Type.OUTPUT, doc, mandatory);
   }
 
   @Override
   public Descriptor outputListAttribute(Boolean allowEmpty, SkylarkList<?> defaultList, String doc,
       Boolean mandatory, Boolean nonEmpty, FuncallExpression ast, Environment env)
       throws EvalException {
-    return new FakeDescriptor(doc);
+    return new FakeDescriptor(Type.OUTPUT_LIST, doc, mandatory);
   }
 
   @Override
   public Descriptor stringDictAttribute(Boolean allowEmpty, SkylarkDict<?, ?> defaultO, String doc,
       Boolean mandatory, Boolean nonEmpty, FuncallExpression ast, Environment env)
       throws EvalException {
-    return new FakeDescriptor(doc);
+    return new FakeDescriptor(Type.STRING_DICT, doc, mandatory);
   }
 
   @Override
   public Descriptor stringListDictAttribute(Boolean allowEmpty, SkylarkDict<?, ?> defaultO,
       String doc, Boolean mandatory, Boolean nonEmpty, FuncallExpression ast, Environment env)
       throws EvalException {
-    return new FakeDescriptor(doc);
+    return new FakeDescriptor(Type.STRING_LIST_DICT, doc, mandatory);
   }
 
   @Override
   public Descriptor licenseAttribute(Object defaultO, String doc, Boolean mandatory,
       FuncallExpression ast, Environment env) throws EvalException {
-    return new FakeDescriptor(doc);
+    return new FakeDescriptor(Type.LICENSE, doc, mandatory);
   }
 
   @Override
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 9b1081b..4260032 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,7 +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.events.Location;
 import com.google.devtools.build.lib.skylarkbuildapi.FileApi;
@@ -29,10 +29,11 @@
 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.skydoc.fakebuildapi.FakeDescriptor.Type;
 import com.google.devtools.build.skydoc.rendering.AttributeInfo;
 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;
 
@@ -44,6 +45,8 @@
  */
 public class FakeSkylarkRuleFunctionsApi implements SkylarkRuleFunctionsApi<FileApi> {
 
+  private static final FakeDescriptor IMPLICIT_NAME_ATTRIBUTE_DESCRIPTOR =
+      new FakeDescriptor(Type.STRING, "A unique name for this rule.", true);
   private final List<RuleInfo> ruleInfoList;
 
   /**
@@ -69,20 +72,22 @@
       Boolean executionPlatformConstraintsAllowed, SkylarkList<?> execCompatibleWith,
       FuncallExpression ast, Environment funcallEnv) throws EvalException {
     List<AttributeInfo> attrInfos;
-    // TODO(cparsons): Include implicit "Name" attribute.
+    ImmutableMap.Builder<String, FakeDescriptor> attrsMapBuilder = ImmutableMap.builder();
     if (attrs != null && attrs != Runtime.NONE) {
       SkylarkDict<?, ?> attrsDict = (SkylarkDict<?, ?>) attrs;
-      Map<String, FakeDescriptor> attrsMap =
-          attrsDict.getContents(String.class, FakeDescriptor.class, "attrs");
-      // TODO(cparsons): Include better attribute details. For example, attribute type.
-      attrInfos = attrsMap.entrySet().stream()
-          .map(entry -> new AttributeInfo(entry.getKey(), entry.getValue().getDocString()))
-          .sorted((o1, o2) -> o1.getName().compareTo(o2.getName()))
-          .collect(Collectors.toList());
-    } else {
-      attrInfos = ImmutableList.of();
+      attrsMapBuilder.putAll(attrsDict.getContents(String.class, FakeDescriptor.class, "attrs"));
     }
 
+    attrsMapBuilder.put("name", IMPLICIT_NAME_ATTRIBUTE_DESCRIPTOR);
+    attrInfos = attrsMapBuilder.build().entrySet().stream()
+        .map(entry -> new AttributeInfo(
+            entry.getKey(),
+            entry.getValue().getDocString(),
+            entry.getValue().getType().getDescription(),
+            entry.getValue().isMandatory()))
+        .collect(Collectors.toList());
+    attrInfos.sort(new AttributeNameComparator());
+
     RuleDefinitionIdentifier functionIdentifier = new RuleDefinitionIdentifier();
 
     ruleInfoList.add(new RuleInfo(functionIdentifier, ast.getLocation(), doc, attrInfos));
@@ -128,4 +133,22 @@
       return this == other;
     }
   }
+
+  /**
+   * A comparator for {@link AttributeInfo} objects which sorts by attribute name alphabetically,
+   * except that any attribute named "name" is placed first.
+   */
+  private static class AttributeNameComparator implements Comparator<AttributeInfo> {
+
+    @Override
+    public int compare(AttributeInfo o1, AttributeInfo o2) {
+      if (o1.getName().equals("name")) {
+        return o2.getName().equals("name") ? 0 : -1;
+      } else if (o2.getName().equals("name")) {
+        return 1;
+      } else {
+        return o1.getName().compareTo(o2.getName());
+      }
+    }
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/AttributeInfo.java b/src/main/java/com/google/devtools/build/skydoc/rendering/AttributeInfo.java
index 8051057..696fe2d 100644
--- a/src/main/java/com/google/devtools/build/skydoc/rendering/AttributeInfo.java
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/AttributeInfo.java
@@ -21,17 +21,36 @@
 
   private final String name;
   private final String docString;
+  private final String typeString;
+  private final boolean mandatory;
 
-  public AttributeInfo(String name, String docString) {
+  public AttributeInfo(String name, String docString, String typeString, boolean mandatory) {
     this.name = name;
     this.docString = docString;
+    this.typeString = typeString;
+    this.mandatory = mandatory;
   }
 
+  @SuppressWarnings("unused") // Used by markdown template.
   public String getName() {
     return name;
   }
 
+  @SuppressWarnings("unused") // Used by markdown template.
   public String getDocString() {
     return docString;
   }
+
+  @SuppressWarnings("unused") // Used by markdown template.
+  public String getTypeString() {
+    return typeString;
+  }
+
+  /**
+   * Returns a string representing whether this attribute is required or optional.
+   */
+  @SuppressWarnings("unused") // Used by markdown template.
+  public String getMandatoryString() {
+    return mandatory ? "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 f9f5495..9632d53 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
@@ -33,7 +33,7 @@
 public class MarkdownRenderer {
 
   private static final String TEMPLATE_FILENAME =
-      "com/google/devtools/build/skydoc/rendering/templates/test.vm";
+      "com/google/devtools/build/skydoc/rendering/templates/rule.vm";
 
   private final VelocityEngine velocityEngine;
 
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/templates/test.vm b/src/main/java/com/google/devtools/build/skydoc/rendering/templates/rule.vm
similarity index 71%
rename from src/main/java/com/google/devtools/build/skydoc/rendering/templates/test.vm
rename to src/main/java/com/google/devtools/build/skydoc/rendering/templates/rule.vm
index b1600e9..cd5ebaf 100644
--- a/src/main/java/com/google/devtools/build/skydoc/rendering/templates/test.vm
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/templates/rule.vm
@@ -19,7 +19,14 @@
 #foreach ($attribute in $ruleInfo.attributes)
     <tr id="#${ruleName}_${attribute.name}">
       <td><code>${attribute.name}</code></td>
-      <td>${attribute.docString}</td>
+      <td>
+        ${attribute.typeString}; ${attribute.mandatoryString}
+#if (!$attribute.docString.isEmpty())
+        <p>
+          ${attribute.docString}
+        </p>
+#end
+      </td>
     </tr>
 #end
   </tbody>
diff --git a/src/test/java/com/google/devtools/build/skydoc/BUILD b/src/test/java/com/google/devtools/build/skydoc/BUILD
index c12329f..31b365d 100644
--- a/src/test/java/com/google/devtools/build/skydoc/BUILD
+++ b/src/test/java/com/google/devtools/build/skydoc/BUILD
@@ -22,6 +22,7 @@
         "//src/main/java/com/google/devtools/build/lib:syntax",
         "//src/main/java/com/google/devtools/build/lib/vfs",
         "//src/main/java/com/google/devtools/build/skydoc:skydoc_lib",
+        "//src/main/java/com/google/devtools/build/skydoc/fakebuildapi",
         "//src/main/java/com/google/devtools/build/skydoc/rendering",
         "//src/test/java/com/google/devtools/build/lib:testutil",
         "//src/test/java/com/google/devtools/build/lib/skylark:testutil",
@@ -97,3 +98,10 @@
     input_file = "testdata/misc_apis_test/input.bzl",
     skydoc = "//src/main/java/com/google/devtools/build/skydoc",
 )
+
+skydoc_test(
+    name = "attribute_types_test",
+    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",
+)
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 89327e3..5bf7a95 100644
--- a/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java
+++ b/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java
@@ -24,6 +24,7 @@
 import com.google.devtools.build.lib.syntax.ParserInputSource;
 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 java.io.IOException;
 import java.nio.file.Paths;
@@ -67,10 +68,10 @@
         "    doc = 'This is my rule. It does stuff.',",
         "    implementation = rule_impl,",
         "    attrs = {",
-        "        'first': attr.label(mandatory=True, allow_files=True, single_file=True),",
-        "        'second': attr.string_dict(mandatory=True),",
-        "        'third': attr.output(mandatory=True),",
-        "        'fourth': attr.bool(default=False, mandatory=False),",
+        "        'a': attr.label(mandatory=True, allow_files=True, single_file=True),",
+        "        'b': attr.string_dict(mandatory=True),",
+        "        'c': attr.output(mandatory=True),",
+        "        'd': attr.bool(default=False, mandatory=False),",
         "    },",
         ")");
 
@@ -88,7 +89,13 @@
     assertThat(ruleInfo.getKey()).isEqualTo("my_rule");
     assertThat(ruleInfo.getValue().getDocString()).isEqualTo("This is my rule. It does stuff.");
     assertThat(getAttrNames(ruleInfo.getValue())).containsExactly(
-        "first", "fourth", "second", "third").inOrder();
+        "name", "a", "b", "c", "d").inOrder();
+    assertThat(getAttrTypes(ruleInfo.getValue())).containsExactly(
+        Type.STRING.getDescription(),
+        Type.LABEL.getDescription(),
+        Type.STRING_DICT.getDescription(),
+        Type.LABEL.getDescription(),
+        Type.BOOLEAN.getDescription()).inOrder();
     assertThat(unexportedRuleInfos.build()).isEmpty();
   }
 
@@ -97,6 +104,11 @@
         .collect(Collectors.toList());
   }
 
+  private static Iterable<String> getAttrTypes(RuleInfo ruleInfo) {
+    return ruleInfo.getAttributes().stream().map(attr -> attr.getTypeString())
+        .collect(Collectors.toList());
+  }
+
   @Test
   public void testMultipleRuleNames() throws Exception {
     scratch.file(
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/android_basic_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/android_basic_test/golden.txt
index e7c4c5f..a69252b 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/android_basic_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/android_basic_test/golden.txt
@@ -2,7 +2,7 @@
 ## android_related_rule
 
 <pre>
-android_related_rule(first, fourth, second, third)
+android_related_rule(name, first, fourth, second, third)
 </pre>
 
 This rule does android-related things.
@@ -15,21 +15,38 @@
     <col class="col-description" />
   </colgroup>
   <tbody>
+    <tr id="#android_related_rule_name">
+      <td><code>name</code></td>
+      <td>
+        String; required
+        <p>
+          A unique name for this rule.
+        </p>
+      </td>
+    </tr>
     <tr id="#android_related_rule_first">
       <td><code>first</code></td>
-      <td></td>
+      <td>
+        Label; required
+      </td>
     </tr>
     <tr id="#android_related_rule_fourth">
       <td><code>fourth</code></td>
-      <td></td>
+      <td>
+        Boolean; optional
+      </td>
     </tr>
     <tr id="#android_related_rule_second">
       <td><code>second</code></td>
-      <td></td>
+      <td>
+        Dictionary: String -> String; required
+      </td>
     </tr>
     <tr id="#android_related_rule_third">
       <td><code>third</code></td>
-      <td></td>
+      <td>
+        Label; required
+      </td>
     </tr>
   </tbody>
 </table>
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/apple_basic_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/apple_basic_test/golden.txt
index 882f540..4cbc0be 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/apple_basic_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/apple_basic_test/golden.txt
@@ -2,7 +2,7 @@
 ## apple_related_rule
 
 <pre>
-apple_related_rule(first, fourth, second, third)
+apple_related_rule(name, first, fourth, second, third)
 </pre>
 
 This rule does apple-related things.
@@ -15,21 +15,38 @@
     <col class="col-description" />
   </colgroup>
   <tbody>
+    <tr id="#apple_related_rule_name">
+      <td><code>name</code></td>
+      <td>
+        String; required
+        <p>
+          A unique name for this rule.
+        </p>
+      </td>
+    </tr>
     <tr id="#apple_related_rule_first">
       <td><code>first</code></td>
-      <td></td>
+      <td>
+        Label; required
+      </td>
     </tr>
     <tr id="#apple_related_rule_fourth">
       <td><code>fourth</code></td>
-      <td></td>
+      <td>
+        Boolean; optional
+      </td>
     </tr>
     <tr id="#apple_related_rule_second">
       <td><code>second</code></td>
-      <td></td>
+      <td>
+        Dictionary: String -> String; required
+      </td>
     </tr>
     <tr id="#apple_related_rule_third">
       <td><code>third</code></td>
-      <td></td>
+      <td>
+        Label; required
+      </td>
     </tr>
   </tbody>
 </table>
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/attribute_types_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/attribute_types_test/golden.txt
new file mode 100644
index 0000000..746e3fe
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/attribute_types_test/golden.txt
@@ -0,0 +1,147 @@
+<a name="#my_rule"></a>
+## my_rule
+
+<pre>
+my_rule(name, a, b, c, d, e, f, g, h, i, j, k, l, m)
+</pre>
+
+This is my rule. It does stuff.
+
+### Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="#my_rule_name">
+      <td><code>name</code></td>
+      <td>
+        String; required
+        <p>
+          A unique name for this rule.
+        </p>
+      </td>
+    </tr>
+    <tr id="#my_rule_a">
+      <td><code>a</code></td>
+      <td>
+        Boolean; required
+        <p>
+          Some bool
+        </p>
+      </td>
+    </tr>
+    <tr id="#my_rule_b">
+      <td><code>b</code></td>
+      <td>
+        Integer; required
+        <p>
+          Some int
+        </p>
+      </td>
+    </tr>
+    <tr id="#my_rule_c">
+      <td><code>c</code></td>
+      <td>
+        List of integers; required
+        <p>
+          Some int_list
+        </p>
+      </td>
+    </tr>
+    <tr id="#my_rule_d">
+      <td><code>d</code></td>
+      <td>
+        Label; required
+        <p>
+          Some label
+        </p>
+      </td>
+    </tr>
+    <tr id="#my_rule_e">
+      <td><code>e</code></td>
+      <td>
+        Dictionary: Label -> String; required
+        <p>
+          Some label_keyed_string_dict
+        </p>
+      </td>
+    </tr>
+    <tr id="#my_rule_f">
+      <td><code>f</code></td>
+      <td>
+        List of labels; required
+        <p>
+          Some label_list
+        </p>
+      </td>
+    </tr>
+    <tr id="#my_rule_g">
+      <td><code>g</code></td>
+      <td>
+        List of strings; optional
+        <p>
+          Some license
+        </p>
+      </td>
+    </tr>
+    <tr id="#my_rule_h">
+      <td><code>h</code></td>
+      <td>
+        Label; optional
+        <p>
+          Some output
+        </p>
+      </td>
+    </tr>
+    <tr id="#my_rule_i">
+      <td><code>i</code></td>
+      <td>
+        List of labels; optional
+        <p>
+          Some output_list
+        </p>
+      </td>
+    </tr>
+    <tr id="#my_rule_j">
+      <td><code>j</code></td>
+      <td>
+        String; required
+        <p>
+          Some string
+        </p>
+      </td>
+    </tr>
+    <tr id="#my_rule_k">
+      <td><code>k</code></td>
+      <td>
+        Dictionary: String -> String; required
+        <p>
+          Some string_dict
+        </p>
+      </td>
+    </tr>
+    <tr id="#my_rule_l">
+      <td><code>l</code></td>
+      <td>
+        List of strings; required
+        <p>
+          Some string_list
+        </p>
+      </td>
+    </tr>
+    <tr id="#my_rule_m">
+      <td><code>m</code></td>
+      <td>
+        Dictionary: String -> List of strings; optional
+        <p>
+          Some string_list_dict
+        </p>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/attribute_types_test/input.bzl b/src/test/java/com/google/devtools/build/skydoc/testdata/attribute_types_test/input.bzl
new file mode 100644
index 0000000..edcef5f
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/attribute_types_test/input.bzl
@@ -0,0 +1,22 @@
+def my_rule_impl(ctx):
+    return struct()
+
+my_rule = rule(
+    implementation = my_rule_impl,
+    doc = "This is my rule. It does stuff.",
+    attrs = {
+        "a": attr.bool(mandatory = True, doc = "Some bool"),
+        "b": attr.int(mandatory = True, doc = "Some int"),
+        "c": attr.int_list(mandatory = True, doc = "Some int_list"),
+        "d": attr.label(mandatory = True, doc = "Some label"),
+        "e": attr.label_keyed_string_dict(mandatory = True, doc = "Some label_keyed_string_dict"),
+        "f": attr.label_list(mandatory = True, doc = "Some label_list"),
+        "g": attr.license(mandatory = False, doc = "Some license"),
+        "h": attr.output(mandatory = False, doc = "Some output"),
+        "i": attr.output_list(mandatory = False, doc = "Some output_list"),
+        "j": attr.string(mandatory = True, doc = "Some string"),
+        "k": attr.string_dict(mandatory = True, doc = "Some string_dict"),
+        "l": attr.string_list(mandatory = True, doc = "Some string_list"),
+        "m": attr.string_list_dict(mandatory = False, doc = "Some string_list_dict"),
+    },
+)
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/cpp_basic_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/cpp_basic_test/golden.txt
index 9674b82..ee379d9 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/cpp_basic_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/cpp_basic_test/golden.txt
@@ -2,7 +2,7 @@
 ## cpp_related_rule
 
 <pre>
-cpp_related_rule(first, fourth, second, third)
+cpp_related_rule(name, first, fourth, second, third)
 </pre>
 
 This rule does cpp-related things.
@@ -15,21 +15,38 @@
     <col class="col-description" />
   </colgroup>
   <tbody>
+    <tr id="#cpp_related_rule_name">
+      <td><code>name</code></td>
+      <td>
+        String; required
+        <p>
+          A unique name for this rule.
+        </p>
+      </td>
+    </tr>
     <tr id="#cpp_related_rule_first">
       <td><code>first</code></td>
-      <td></td>
+      <td>
+        Label; required
+      </td>
     </tr>
     <tr id="#cpp_related_rule_fourth">
       <td><code>fourth</code></td>
-      <td></td>
+      <td>
+        Boolean; optional
+      </td>
     </tr>
     <tr id="#cpp_related_rule_second">
       <td><code>second</code></td>
-      <td></td>
+      <td>
+        Dictionary: String -> String; required
+      </td>
     </tr>
     <tr id="#cpp_related_rule_third">
       <td><code>third</code></td>
-      <td></td>
+      <td>
+        Label; required
+      </td>
     </tr>
   </tbody>
 </table>
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/java_basic_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/java_basic_test/golden.txt
index 7ab9fc0..83f6ea4 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/java_basic_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/java_basic_test/golden.txt
@@ -2,7 +2,7 @@
 ## java_related_rule
 
 <pre>
-java_related_rule(first, fourth, second, third)
+java_related_rule(name, first, fourth, second, third)
 </pre>
 
 This rule does java-related things.
@@ -15,21 +15,38 @@
     <col class="col-description" />
   </colgroup>
   <tbody>
+    <tr id="#java_related_rule_name">
+      <td><code>name</code></td>
+      <td>
+        String; required
+        <p>
+          A unique name for this rule.
+        </p>
+      </td>
+    </tr>
     <tr id="#java_related_rule_first">
       <td><code>first</code></td>
-      <td></td>
+      <td>
+        Label; required
+      </td>
     </tr>
     <tr id="#java_related_rule_fourth">
       <td><code>fourth</code></td>
-      <td></td>
+      <td>
+        Boolean; optional
+      </td>
     </tr>
     <tr id="#java_related_rule_second">
       <td><code>second</code></td>
-      <td></td>
+      <td>
+        Dictionary: String -> String; required
+      </td>
     </tr>
     <tr id="#java_related_rule_third">
       <td><code>third</code></td>
-      <td></td>
+      <td>
+        Label; required
+      </td>
     </tr>
   </tbody>
 </table>
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 0619f29..810c90b 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
@@ -2,7 +2,7 @@
 ## my_rule
 
 <pre>
-my_rule(first, fourth, second, third)
+my_rule(name, first, fourth, second, third)
 </pre>
 
 This rule exercises some of the build API.
@@ -15,21 +15,38 @@
     <col class="col-description" />
   </colgroup>
   <tbody>
+    <tr id="#my_rule_name">
+      <td><code>name</code></td>
+      <td>
+        String; required
+        <p>
+          A unique name for this rule.
+        </p>
+      </td>
+    </tr>
     <tr id="#my_rule_first">
       <td><code>first</code></td>
-      <td></td>
+      <td>
+        Label; required
+      </td>
     </tr>
     <tr id="#my_rule_fourth">
       <td><code>fourth</code></td>
-      <td></td>
+      <td>
+        Boolean; optional
+      </td>
     </tr>
     <tr id="#my_rule_second">
       <td><code>second</code></td>
-      <td></td>
+      <td>
+        Dictionary: String -> String; required
+      </td>
     </tr>
     <tr id="#my_rule_third">
       <td><code>third</code></td>
-      <td></td>
+      <td>
+        Label; required
+      </td>
     </tr>
   </tbody>
 </table>
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 1b2d0a0..771e502 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
@@ -2,7 +2,7 @@
 ## my_rule
 
 <pre>
-my_rule(first, second)
+my_rule(name, first, second)
 </pre>
 
 This is my rule. It does stuff.
@@ -15,13 +15,29 @@
     <col class="col-description" />
   </colgroup>
   <tbody>
+    <tr id="#my_rule_name">
+      <td><code>name</code></td>
+      <td>
+        String; required
+        <p>
+          A unique name for this rule.
+        </p>
+      </td>
+    </tr>
     <tr id="#my_rule_first">
       <td><code>first</code></td>
-      <td>first my_rule doc string</td>
+      <td>
+        Label; required
+        <p>
+          first my_rule doc string
+        </p>
+      </td>
     </tr>
     <tr id="#my_rule_second">
       <td><code>second</code></td>
-      <td></td>
+      <td>
+        Dictionary: String -> String; required
+      </td>
     </tr>
   </tbody>
 </table>
@@ -31,7 +47,7 @@
 ## other_rule
 
 <pre>
-other_rule(fourth, third)
+other_rule(name, fourth, third)
 </pre>
 
 This is another rule.
@@ -44,13 +60,29 @@
     <col class="col-description" />
   </colgroup>
   <tbody>
+    <tr id="#other_rule_name">
+      <td><code>name</code></td>
+      <td>
+        String; required
+        <p>
+          A unique name for this rule.
+        </p>
+      </td>
+    </tr>
     <tr id="#other_rule_fourth">
       <td><code>fourth</code></td>
-      <td></td>
+      <td>
+        Dictionary: String -> String; required
+      </td>
     </tr>
     <tr id="#other_rule_third">
       <td><code>third</code></td>
-      <td>third other_rule doc string</td>
+      <td>
+        Label; required
+        <p>
+          third other_rule doc string
+        </p>
+      </td>
     </tr>
   </tbody>
 </table>
@@ -60,7 +92,7 @@
 ## yet_another_rule
 
 <pre>
-yet_another_rule(fifth)
+yet_another_rule(name, fifth)
 </pre>
 
 This is yet another rule
@@ -73,9 +105,20 @@
     <col class="col-description" />
   </colgroup>
   <tbody>
+    <tr id="#yet_another_rule_name">
+      <td><code>name</code></td>
+      <td>
+        String; required
+        <p>
+          A unique name for this rule.
+        </p>
+      </td>
+    </tr>
     <tr id="#yet_another_rule_fifth">
       <td><code>fifth</code></td>
-      <td></td>
+      <td>
+        Label; required
+      </td>
     </tr>
   </tbody>
 </table>
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 7d37f79..3bf31da 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
@@ -2,7 +2,7 @@
 ## my_rule
 
 <pre>
-my_rule(first, second)
+my_rule(name, first, second)
 </pre>
 
 This is my rule. It does stuff.
@@ -15,13 +15,26 @@
     <col class="col-description" />
   </colgroup>
   <tbody>
+    <tr id="#my_rule_name">
+      <td><code>name</code></td>
+      <td>
+        String; required
+        <p>
+          A unique name for this rule.
+        </p>
+      </td>
+    </tr>
     <tr id="#my_rule_first">
       <td><code>first</code></td>
-      <td></td>
+      <td>
+        Label; required
+      </td>
     </tr>
     <tr id="#my_rule_second">
       <td><code>second</code></td>
-      <td></td>
+      <td>
+        Dictionary: String -> String; required
+      </td>
     </tr>
   </tbody>
 </table>
@@ -31,7 +44,7 @@
 ## other_rule
 
 <pre>
-other_rule(fourth, third)
+other_rule(name, fourth, third)
 </pre>
 
 This is another rule.
@@ -44,13 +57,26 @@
     <col class="col-description" />
   </colgroup>
   <tbody>
+    <tr id="#other_rule_name">
+      <td><code>name</code></td>
+      <td>
+        String; required
+        <p>
+          A unique name for this rule.
+        </p>
+      </td>
+    </tr>
     <tr id="#other_rule_fourth">
       <td><code>fourth</code></td>
-      <td></td>
+      <td>
+        Dictionary: String -> String; required
+      </td>
     </tr>
     <tr id="#other_rule_third">
       <td><code>third</code></td>
-      <td></td>
+      <td>
+        Label; required
+      </td>
     </tr>
   </tbody>
 </table>
@@ -60,7 +86,7 @@
 ## yet_another_rule
 
 <pre>
-yet_another_rule(fifth)
+yet_another_rule(name, fifth)
 </pre>
 
 This is yet another rule
@@ -73,9 +99,20 @@
     <col class="col-description" />
   </colgroup>
   <tbody>
+    <tr id="#yet_another_rule_name">
+      <td><code>name</code></td>
+      <td>
+        String; required
+        <p>
+          A unique name for this rule.
+        </p>
+      </td>
+    </tr>
     <tr id="#yet_another_rule_fifth">
       <td><code>fifth</code></td>
-      <td></td>
+      <td>
+        Label; required
+      </td>
     </tr>
   </tbody>
 </table>
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/simple_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/simple_test/golden.txt
index d86c0c7..b9f8cec 100644
--- a/src/test/java/com/google/devtools/build/skydoc/testdata/simple_test/golden.txt
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/simple_test/golden.txt
@@ -2,7 +2,7 @@
 ## my_rule
 
 <pre>
-my_rule(first, fourth, second, third)
+my_rule(name, first, fourth, second, third)
 </pre>
 
 This is my rule. It does stuff.
@@ -15,21 +15,44 @@
     <col class="col-description" />
   </colgroup>
   <tbody>
+    <tr id="#my_rule_name">
+      <td><code>name</code></td>
+      <td>
+        String; required
+        <p>
+          A unique name for this rule.
+        </p>
+      </td>
+    </tr>
     <tr id="#my_rule_first">
       <td><code>first</code></td>
-      <td>first doc string</td>
+      <td>
+        Label; required
+        <p>
+          first doc string
+        </p>
+      </td>
     </tr>
     <tr id="#my_rule_fourth">
       <td><code>fourth</code></td>
-      <td>fourth doc string</td>
+      <td>
+        Boolean; optional
+        <p>
+          fourth doc string
+        </p>
+      </td>
     </tr>
     <tr id="#my_rule_second">
       <td><code>second</code></td>
-      <td></td>
+      <td>
+        Dictionary: String -> String; required
+      </td>
     </tr>
     <tr id="#my_rule_third">
       <td><code>third</code></td>
-      <td></td>
+      <td>
+        Label; required
+      </td>
     </tr>
   </tbody>
 </table>
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 44040ee..4f78cdf 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
@@ -2,7 +2,7 @@
 ## <unknown name>
 
 <pre>
-<unknown name>(first, fourth, second, third)
+<unknown name>(name, first, fourth, second, third)
 </pre>
 
 
@@ -15,21 +15,38 @@
     <col class="col-description" />
   </colgroup>
   <tbody>
+    <tr id="#<unknown name>_name">
+      <td><code>name</code></td>
+      <td>
+        String; required
+        <p>
+          A unique name for this rule.
+        </p>
+      </td>
+    </tr>
     <tr id="#<unknown name>_first">
       <td><code>first</code></td>
-      <td></td>
+      <td>
+        Label; required
+      </td>
     </tr>
     <tr id="#<unknown name>_fourth">
       <td><code>fourth</code></td>
-      <td></td>
+      <td>
+        Boolean; optional
+      </td>
     </tr>
     <tr id="#<unknown name>_second">
       <td><code>second</code></td>
-      <td></td>
+      <td>
+        Dictionary: String -> String; required
+      </td>
     </tr>
     <tr id="#<unknown name>_third">
       <td><code>third</code></td>
-      <td></td>
+      <td>
+        Label; required
+      </td>
     </tr>
   </tbody>
 </table>