Allows Renderer Binary to output aspect information as a markdown output
Depends on CL 259570926.

Work toward https://github.com/bazelbuild/skydoc/issues/196

PiperOrigin-RevId: 260145437
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 c379921..9045fd4 100644
--- a/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
+++ b/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
@@ -173,6 +173,8 @@
       "com/google/devtools/build/skydoc/rendering/templates/provider.vm";
   private static final String FUNCTION_TEMPLATE_PATH =
       "com/google/devtools/build/skydoc/rendering/templates/func.vm";
+  private static final String ASPECT_TEMPLATE_PATH =
+      "com/google/devtools/build/skydoc/rendering/templates/aspect.vm";
 
   public SkydocMain(SkylarkFileAccessor fileAccessor, String workspaceName, List<String> depRoots) {
     this.fileAccessor = fileAccessor;
@@ -268,7 +270,8 @@
               HEADER_TEMPLATE_PATH,
               RULE_TEMPLATE_PATH,
               PROVIDER_TEMPLATE_PATH,
-              FUNCTION_TEMPLATE_PATH);
+              FUNCTION_TEMPLATE_PATH,
+              ASPECT_TEMPLATE_PATH);
       try (PrintWriter printWriter = new PrintWriter(outputPath, "UTF-8")) {
         printWriter.println(renderer.renderMarkdownHeader());
         printRuleInfos(printWriter, renderer, filteredRuleInfos);
diff --git a/src/main/java/com/google/devtools/build/skydoc/renderer/RendererMain.java b/src/main/java/com/google/devtools/build/skydoc/renderer/RendererMain.java
index ce17b66..1a58afc 100644
--- a/src/main/java/com/google/devtools/build/skydoc/renderer/RendererMain.java
+++ b/src/main/java/com/google/devtools/build/skydoc/renderer/RendererMain.java
@@ -15,6 +15,7 @@
 package com.google.devtools.build.skydoc.renderer;
 
 import com.google.devtools.build.skydoc.rendering.MarkdownRenderer;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AspectInfo;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ModuleInfo;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ProviderInfo;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.RuleInfo;
@@ -50,16 +51,22 @@
     String ruleTemplatePath = rendererOptions.ruleTemplateFilePath;
     String providerTemplatePath = rendererOptions.providerTemplateFilePath;
     String funcTemplatePath = rendererOptions.funcTemplateFilePath;
+    String aspectTemplatePath = rendererOptions.aspectTemplateFilePath;
 
     MarkdownRenderer renderer =
         new MarkdownRenderer(
-            headerTemplatePath, ruleTemplatePath, providerTemplatePath, funcTemplatePath);
+            headerTemplatePath,
+            ruleTemplatePath,
+            providerTemplatePath,
+            funcTemplatePath,
+            aspectTemplatePath);
     try (PrintWriter printWriter = new PrintWriter(outputPath, "UTF-8")) {
       ModuleInfo moduleInfo = ModuleInfo.parseFrom(new FileInputStream(inputPath));
       printWriter.println(renderer.renderMarkdownHeader());
       printRuleInfos(printWriter, renderer, moduleInfo.getRuleInfoList());
       printProviderInfos(printWriter, renderer, moduleInfo.getProviderInfoList());
       printUserDefinedFunctions(printWriter, renderer, moduleInfo.getFuncInfoList());
+      printAspectInfos(printWriter, renderer, moduleInfo.getAspectInfoList());
     } catch (InvalidProtocolBufferException e) {
       throw new IllegalArgumentException("Input file is not a valid ModuleInfo proto.", e);
     }
@@ -93,4 +100,13 @@
       printWriter.println();
     }
   }
+
+  private static void printAspectInfos(
+      PrintWriter printWriter, MarkdownRenderer renderer, List<AspectInfo> aspectInfos)
+      throws IOException {
+    for (AspectInfo aspectProto : aspectInfos) {
+      printWriter.println(renderer.render(aspectProto.getAspectName(), aspectProto));
+      printWriter.println();
+    }
+  }
 }
diff --git a/src/main/java/com/google/devtools/build/skydoc/renderer/RendererOptions.java b/src/main/java/com/google/devtools/build/skydoc/renderer/RendererOptions.java
index b4f3a6b..730cb06 100644
--- a/src/main/java/com/google/devtools/build/skydoc/renderer/RendererOptions.java
+++ b/src/main/java/com/google/devtools/build/skydoc/renderer/RendererOptions.java
@@ -77,4 +77,14 @@
           "The template for the documentation of a function. If the option is"
               + " unspecified, a default markdown output template will be used.")
   public String funcTemplateFilePath;
+
+  @Option(
+      name = "aspect_template",
+      defaultValue = "com/google/devtools/build/skydoc/rendering/templates/aspect.vm",
+      documentationCategory = OptionDocumentationCategory.UNDOCUMENTED,
+      effectTags = OptionEffectTag.UNKNOWN,
+      help =
+          "The template for the documentation of a aspect. If the option is unspecified, a"
+              + " default markdown output template will be used.")
+  public String aspectTemplateFilePath;
 }
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 8334f69..4f2dd8c 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
@@ -16,6 +16,7 @@
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AspectInfo;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ProviderInfo;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.RuleInfo;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.UserDefinedFunctionInfo;
@@ -45,6 +46,7 @@
   private final String ruleTemplateFilename;
   private final String providerTemplateFilename;
   private final String functionTemplateFilename;
+  private final String aspectTemplateFilename;
 
   private final VelocityEngine velocityEngine;
 
@@ -52,12 +54,14 @@
       String headerTemplate,
       String ruleTemplate,
       String providerTemplate,
-      String functionTemplate) {
+      String functionTemplate,
+      String aspectTemplate) {
     this.headerTemplateFilename = headerTemplate;
     this.ruleTemplateFilename = ruleTemplate;
     this.providerTemplateFilename = providerTemplate;
     this.functionTemplateFilename = functionTemplate;
-    
+    this.aspectTemplateFilename = aspectTemplate;
+
     this.velocityEngine = new VelocityEngine();
     velocityEngine.setProperty("resource.loader", "classpath, jar");
     velocityEngine.setProperty("classpath.resource.loader.class",
@@ -143,6 +147,25 @@
   }
 
   /**
+   * Returns a markdown rendering of aspect documentation for the given aspect information object
+   * with the given aspect name.
+   */
+  public String render(String aspectName, AspectInfo aspectInfo) throws IOException {
+    VelocityContext context = new VelocityContext();
+    context.put("util", new MarkdownUtil());
+    context.put("aspectName", aspectName);
+    context.put("aspectInfo", aspectInfo);
+
+    StringWriter stringWriter = new StringWriter();
+    Reader reader = readerFromPath(aspectTemplateFilename);
+    try {
+      velocityEngine.evaluate(context, stringWriter, aspectTemplateFilename, reader);
+    } catch (ResourceNotFoundException | ParseErrorException | MethodInvocationException e) {
+      throw new IOException(e);
+    }
+    return stringWriter.toString();
+  }
+  /**
    * Returns a reader from the given path.
    *
    * @param filePath The given path, either a filesystem path or a java Resource
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 06c969b..44e6e45 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
@@ -15,6 +15,7 @@
 package com.google.devtools.build.skydoc.rendering;
 
 import com.google.common.base.Joiner;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AspectInfo;
 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.FunctionParamInfo;
@@ -68,6 +69,20 @@
   }
 
   /**
+   * Return a string representing the aspect summary for the given aspect with the given name.
+   *
+   * <p>For example: 'my_aspect(foo, bar)'. The summary will contain hyperlinks for each attribute.
+   */
+  @SuppressWarnings("unused") // Used by markdown template.
+  public String aspectSummary(String aspectName, AspectInfo aspectInfo) {
+    List<String> attributeNames =
+        aspectInfo.getAttributeList().stream()
+            .map(attr -> attr.getName())
+            .collect(Collectors.toList());
+    return summary(aspectName, attributeNames);
+  }
+
+  /**
    * Return a string representing the summary for the given user-defined function.
    *
    * For example: 'my_func(foo, bar)'.
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/templates/aspect.vm b/src/main/java/com/google/devtools/build/skydoc/rendering/templates/aspect.vm
new file mode 100644
index 0000000..35e5441
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/templates/aspect.vm
@@ -0,0 +1,56 @@
+<a name="#${aspectName}"></a>
+
+#[[##]]# ${aspectName}
+
+<pre>
+${util.aspectSummary($aspectName, $aspectInfo)}
+</pre>
+
+$aspectInfo.getDocString()
+
+#[[###]]# Aspect Attributes
+
+#if (!$aspectInfo.getAspectAttributeList().isEmpty())
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+#foreach ($aspectAttribute in $aspectInfo.getAspectAttributeList())
+    <tr id="${aspectName}-${aspectAttribute}">
+      <td><code>${aspectAttribute}</code></td>
+      <td>
+        String; required.
+#end
+      </td>
+    </tr>
+#end
+  </tbody>
+</table>
+
+#[[###]]# Attributes
+
+#if (!$aspectInfo.getAttributeList().isEmpty())
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+#foreach ($attribute in $aspectInfo.getAttributeList())
+    <tr id="${aspectName}-${attribute.name}">
+      <td><code>${attribute.name}</code></td>
+      <td>
+        ${util.attributeTypeString($attribute)}; ${util.mandatoryString($attribute)}
+#if (!$attribute.docString.isEmpty())
+        <p>
+          ${attribute.docString.trim()}
+        </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 d5b6ecc..7c0c9e2 100644
--- a/src/test/java/com/google/devtools/build/skydoc/BUILD
+++ b/src/test/java/com/google/devtools/build/skydoc/BUILD
@@ -17,6 +17,7 @@
 filegroup(
     name = "test_template_files",
     srcs = [
+        "//src/test/java/com/google/devtools/build/skydoc:test_templates/aspect.vm",
         "//src/test/java/com/google/devtools/build/skydoc:test_templates/func.vm",
         "//src/test/java/com/google/devtools/build/skydoc:test_templates/header.vm",
         "//src/test/java/com/google/devtools/build/skydoc:test_templates/provider.vm",
@@ -247,6 +248,12 @@
     input_file = "testdata/struct_default_value_test/input.bzl",
 )
 
+skydoc_test(
+    name = "aspect_test",
+    golden_file = "testdata/aspect_test/golden.txt",
+    input_file = "testdata/aspect_test/input.bzl",
+)
+
 genrule(
     name = "generate_bzl_test_dep",
     srcs = ["testdata/generated_bzl_test/dep.bzl.tpl"],
diff --git a/src/test/java/com/google/devtools/build/skydoc/MarkdownRendererTest.java b/src/test/java/com/google/devtools/build/skydoc/MarkdownRendererTest.java
index 2bf849a..440d9c1 100644
--- a/src/test/java/com/google/devtools/build/skydoc/MarkdownRendererTest.java
+++ b/src/test/java/com/google/devtools/build/skydoc/MarkdownRendererTest.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import com.google.devtools.build.skydoc.rendering.MarkdownRenderer;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AspectInfo;
 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.FunctionParamInfo;
@@ -39,9 +40,15 @@
   private final String providerTemplatePath =
       "com/google/devtools/build/skydoc/test_templates/provider.vm";
   private final String funcTemplatePath = "com/google/devtools/build/skydoc/test_templates/func.vm";
+  private final String aspectTemplatePath =
+      "com/google/devtools/build/skydoc/test_templates/aspect.vm";
   private final MarkdownRenderer renderer =
       new MarkdownRenderer(
-          headerTemplatePath, ruleTemplatePath, providerTemplatePath, funcTemplatePath);
+          headerTemplatePath,
+          ruleTemplatePath,
+          providerTemplatePath,
+          funcTemplatePath,
+          aspectTemplatePath);
 
   @Test
   public void testHeaderStrings() throws IOException {
@@ -150,4 +157,46 @@
                 + "          the first parameter\n"
                 + "        </p>\n");
   }
+
+  @Test
+  public void testAspectStrings() throws IOException {
+    AttributeInfo attrInfo =
+        AttributeInfo.newBuilder()
+            .setName("first")
+            .setDocString("the first attribute")
+            .setTypeValue(AttributeType.STRING.getNumber())
+            .build();
+    AspectInfo aspectInfo =
+        AspectInfo.newBuilder()
+            .setAspectName("my_aspect")
+            .setDocString("This aspect does things.")
+            .addAttribute(attrInfo)
+            .addAspectAttribute("deps")
+            .build();
+
+    assertThat(renderer.render(aspectInfo.getAspectName(), aspectInfo))
+        .isEqualTo(
+            "<a name=\"#my_aspect\"></a>\n"
+                + "\n"
+                + "## my_aspect\n"
+                + "\n"
+                + "<pre>\n"
+                + "null(<a href=\"#null-first\">first</a>)\n"
+                + "</pre>\n"
+                + "\n"
+                + "This aspect does things.\n"
+                + "\n"
+                + "### Aspect Attributes\n"
+                + "\n"
+                + "        <code>deps</code><\n"
+                + "        String; required.\n"
+                + "\n"
+                + "### Attributes\n"
+                + "\n"
+                + "      <code>first</code>\n"
+                + "      String; optional\n"
+                + "        <p>\n"
+                + "          the first attribute\n"
+                + "        </p>\n");
+  }
 }
diff --git a/src/test/java/com/google/devtools/build/skydoc/test_templates/aspect.vm b/src/test/java/com/google/devtools/build/skydoc/test_templates/aspect.vm
new file mode 100644
index 0000000..747f073
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/test_templates/aspect.vm
@@ -0,0 +1,32 @@
+<a name="#${aspectName}"></a>
+
+#[[##]]# ${aspectName}
+
+<pre>
+${util.aspectSummary(aspectName, $aspectInfo)}
+</pre>
+
+$aspectInfo.getDocString()
+
+#[[###]]# Aspect Attributes
+
+#if (!$aspectInfo.getAspectAttributeList().isEmpty())
+#foreach ($aspectAttribute in $aspectInfo.getAspectAttributeList())
+        <code>${aspectAttribute}</code><
+        String; required.
+#end
+#end
+
+#[[###]]# Attributes
+
+#if (!$aspectInfo.getAttributeList().isEmpty())
+#foreach ($attribute in $aspectInfo.getAttributeList())
+      <code>${attribute.name}</code>
+      ${util.attributeTypeString($attribute)}; ${util.mandatoryString($attribute)}
+#if (!$attribute.docString.isEmpty())
+        <p>
+          ${attribute.docString.trim()}
+        </p>
+#end
+#end
+#end
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/aspect_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/testdata/aspect_test/golden.txt
new file mode 100644
index 0000000..a4d2093
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/aspect_test/golden.txt
@@ -0,0 +1,148 @@
+<!-- Generated with Stardoc: http://skydoc.bazel.build -->
+
+<a name="#my_aspect_impl"></a>
+
+## my_aspect_impl
+
+<pre>
+my_aspect_impl(<a href="#my_aspect_impl-ctx">ctx</a>)
+</pre>
+
+
+
+### Parameters
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="my_aspect_impl-ctx">
+      <td><code>ctx</code></td>
+      <td>
+        required.
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+
+<a name="#my_aspect"></a>
+
+## my_aspect
+
+<pre>
+my_aspect(<a href="#my_aspect-name">name</a>, <a href="#my_aspect-first">first</a>, <a href="#my_aspect-second">second</a>)
+</pre>
+
+This is my aspect. It does stuff.
+
+### Aspect Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="my_aspect-deps">
+      <td><code>deps</code></td>
+      <td>
+        String; required.
+    <tr id="my_aspect-attr_aspect">
+      <td><code>attr_aspect</code></td>
+      <td>
+        String; required.
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+### Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="my_aspect-name">
+      <td><code>name</code></td>
+      <td>
+        <a href="https://bazel.build/docs/build-ref.html#name">Name</a>; required
+        <p>
+          A unique name for this target.
+        </p>
+      </td>
+    </tr>
+    <tr id="my_aspect-first">
+      <td><code>first</code></td>
+      <td>
+        <a href="https://bazel.build/docs/build-ref.html#labels">Label</a>; required
+      </td>
+    </tr>
+    <tr id="my_aspect-second">
+      <td><code>second</code></td>
+      <td>
+        <a href="https://bazel.build/docs/skylark/lib/dict.html">Dictionary: String -> String</a>; required
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+
+<a name="#other_aspect"></a>
+
+## other_aspect
+
+<pre>
+other_aspect(<a href="#other_aspect-name">name</a>, <a href="#other_aspect-third">third</a>)
+</pre>
+
+This is another aspect.
+
+### Aspect Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="other_aspect-*">
+      <td><code>*</code></td>
+      <td>
+        String; required.
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+### Attributes
+
+<table class="params-table">
+  <colgroup>
+    <col class="col-param" />
+    <col class="col-description" />
+  </colgroup>
+  <tbody>
+    <tr id="other_aspect-name">
+      <td><code>name</code></td>
+      <td>
+        <a href="https://bazel.build/docs/build-ref.html#name">Name</a>; required
+        <p>
+          A unique name for this target.
+        </p>
+      </td>
+    </tr>
+    <tr id="other_aspect-third">
+      <td><code>third</code></td>
+      <td>
+        Integer; required
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+
diff --git a/src/test/java/com/google/devtools/build/skydoc/testdata/aspect_test/input.bzl b/src/test/java/com/google/devtools/build/skydoc/testdata/aspect_test/input.bzl
new file mode 100644
index 0000000..1ffedb4
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/testdata/aspect_test/input.bzl
@@ -0,0 +1,24 @@
+"""The input file for the aspect test"""
+
+def my_aspect_impl(ctx):
+    return []
+
+my_aspect = aspect(
+    implementation = my_aspect_impl,
+    doc = "This is my aspect. It does stuff.",
+    attr_aspects = ["deps", "attr_aspect"],
+    attrs = {
+        "first": attr.label(mandatory = True, allow_single_file = True),
+        "second": attr.string_dict(mandatory = True),
+    },
+)
+
+other_aspect = aspect(
+    implementation = my_aspect_impl,
+    doc = "This is another aspect.",
+    attr_aspects = ["*"],
+    attrs = {
+        "_hidden": attr.string(),
+        "third": attr.int(mandatory = True),
+    },
+)