Allows Stardoc Binary to output aspect information as a proto output

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

PiperOrigin-RevId: 259836027
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 0c68293..e2ece22 100644
--- a/src/main/java/com/google/devtools/build/docgen/SymbolFamilies.java
+++ b/src/main/java/com/google/devtools/build/docgen/SymbolFamilies.java
@@ -181,7 +181,8 @@
             new FakeSkylarkAttrApi(),
             new FakeSkylarkCommandLineApi(),
             new FakeSkylarkNativeModuleApi(),
-            new FakeSkylarkRuleFunctionsApi(Lists.newArrayList(), Lists.newArrayList()),
+            new FakeSkylarkRuleFunctionsApi(
+                Lists.newArrayList(), 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 e867333..c379921 100644
--- a/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
+++ b/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
@@ -110,12 +110,14 @@
 import com.google.devtools.build.skydoc.fakebuildapi.test.FakeAnalysisTestResultInfoProvider;
 import com.google.devtools.build.skydoc.fakebuildapi.test.FakeCoverageCommon;
 import com.google.devtools.build.skydoc.fakebuildapi.test.FakeTestingModule;
+import com.google.devtools.build.skydoc.rendering.AspectInfoWrapper;
 import com.google.devtools.build.skydoc.rendering.DocstringParseException;
 import com.google.devtools.build.skydoc.rendering.FunctionUtil;
 import com.google.devtools.build.skydoc.rendering.MarkdownRenderer;
 import com.google.devtools.build.skydoc.rendering.ProtoRenderer;
 import com.google.devtools.build.skydoc.rendering.ProviderInfoWrapper;
 import com.google.devtools.build.skydoc.rendering.RuleInfoWrapper;
+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;
@@ -218,6 +220,7 @@
     ImmutableMap.Builder<String, RuleInfo> ruleInfoMap = ImmutableMap.builder();
     ImmutableMap.Builder<String, ProviderInfo> providerInfoMap = ImmutableMap.builder();
     ImmutableMap.Builder<String, UserDefinedFunction> userDefinedFunctions = ImmutableMap.builder();
+    ImmutableMap.Builder<String, AspectInfo> aspectInfoMap = ImmutableMap.builder();
 
     try {
       new SkydocMain(new FilesystemFileAccessor(), skydocOptions.workspaceName, depRoots)
@@ -226,7 +229,8 @@
               targetFileLabel,
               ruleInfoMap,
               providerInfoMap,
-              userDefinedFunctions);
+              userDefinedFunctions,
+              aspectInfoMap);
     } catch (StarlarkEvaluationException exception) {
       System.err.println("Stardoc documentation generation failed: " + exception.getMessage());
       System.exit(1);
@@ -244,6 +248,10 @@
         userDefinedFunctions.build().entrySet().stream()
             .filter(entry -> validSymbolName(symbolNames, entry.getKey()))
             .collect(ImmutableMap.toImmutableMap(Entry::getKey, Entry::getValue));
+    Map<String, AspectInfo> filteredAspectInfos =
+        aspectInfoMap.build().entrySet().stream()
+            .filter(entry -> validSymbolName(symbolNames, entry.getKey()))
+            .collect(ImmutableMap.toImmutableMap(Entry::getKey, Entry::getValue));
 
     if (skydocOptions.outputFormat == OutputFormat.PROTO) {
       try (BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(outputPath))) {
@@ -251,6 +259,7 @@
             .appendRuleInfos(filteredRuleInfos.values())
             .appendProviderInfos(filteredProviderInfos.values())
             .appendUserDefinedFunctionInfos(filteredUserDefinedFunctions)
+            .appendAspectInfos(filteredAspectInfos.values())
             .writeModuleInfo(out);
       }
     } else if (skydocOptions.outputFormat == OutputFormat.MARKDOWN) {
@@ -357,6 +366,9 @@
    * @param userDefinedFunctionMap a map builder to be populated with user-defined functions. Keys
    *     are exported names of functions, and values are the {@link UserDefinedFunction} objects.
    *     For example, 'def my_function(foo):' is a function with key 'my_function'.
+   * @param aspectInfoMap a map builder to be populated with aspect definition information for named
+   *     aspects. Keys are exported names of aspects, and values are the {@link AspectInfo} asepct
+   *     descriptions. For example, 'my_aspect = aspect(...)' has key 'my_aspect'
    * @throws InterruptedException if evaluation is interrupted
    */
   public Environment eval(
@@ -364,14 +376,19 @@
       Label label,
       ImmutableMap.Builder<String, RuleInfo> ruleInfoMap,
       ImmutableMap.Builder<String, ProviderInfo> providerInfoMap,
-      ImmutableMap.Builder<String, UserDefinedFunction> userDefinedFunctionMap)
+      ImmutableMap.Builder<String, UserDefinedFunction> userDefinedFunctionMap,
+      ImmutableMap.Builder<String, AspectInfo> aspectInfoMap)
       throws InterruptedException, IOException, LabelSyntaxException, EvalException,
           StarlarkEvaluationException {
 
     List<RuleInfoWrapper> ruleInfoList = new ArrayList<>();
 
     List<ProviderInfoWrapper> providerInfoList = new ArrayList<>();
-    Environment env = recursiveEval(semantics, label, ruleInfoList, providerInfoList);
+
+    List<AspectInfoWrapper> aspectInfoList = new ArrayList<>();
+
+    Environment env =
+        recursiveEval(semantics, label, ruleInfoList, providerInfoList, aspectInfoList);
 
     Map<BaseFunction, RuleInfoWrapper> ruleFunctions =
         ruleInfoList.stream()
@@ -382,6 +399,11 @@
         providerInfoList.stream()
             .collect(Collectors.toMap(ProviderInfoWrapper::getIdentifier, Functions.identity()));
 
+    Map<BaseFunction, AspectInfoWrapper> aspectFunctions =
+        aspectInfoList.stream()
+            .collect(
+                Collectors.toMap(AspectInfoWrapper::getIdentifierFunction, Functions.identity()));
+
     // Sort the bindings so their ordering is deterministic.
     TreeMap<String, Object> sortedBindings = new TreeMap<>(env.getGlobals().getExportedBindings());
 
@@ -406,6 +428,12 @@
         FakeStructApi namespace = (FakeStructApi) envEntry.getValue();
         putStructFields(namespaceName, namespace, userDefinedFunctionMap);
       }
+      if (aspectFunctions.containsKey(envEntry.getValue())) {
+        AspectInfo.Builder aspectInfoBuild =
+            aspectFunctions.get(envEntry.getValue()).getAspectInfo();
+        AspectInfo aspectInfo = aspectInfoBuild.setAspectName(envEntry.getKey()).build();
+        aspectInfoMap.put(envEntry.getKey(), aspectInfo);
+      }
     }
 
     return env;
@@ -450,7 +478,8 @@
       StarlarkSemantics semantics,
       Label label,
       List<RuleInfoWrapper> ruleInfoList,
-      List<ProviderInfoWrapper> providerInfoList)
+      List<ProviderInfoWrapper> providerInfoList,
+      List<AspectInfoWrapper> aspectInfoList)
       throws InterruptedException, IOException, LabelSyntaxException, StarlarkEvaluationException {
     Path path = pathOfLabel(label);
 
@@ -473,7 +502,7 @@
 
       try {
         Environment importEnv =
-            recursiveEval(semantics, relativeLabel, ruleInfoList, providerInfoList);
+            recursiveEval(semantics, relativeLabel, ruleInfoList, providerInfoList, aspectInfoList);
         imports.put(anImport.getImportString(), new Extension(importEnv));
       } catch (NoSuchFileException noSuchFileException) {
         throw new StarlarkEvaluationException(
@@ -484,7 +513,8 @@
     }
 
     Environment env =
-        evalSkylarkBody(semantics, buildFileAST, imports, ruleInfoList, providerInfoList);
+        evalSkylarkBody(
+            semantics, buildFileAST, imports, ruleInfoList, providerInfoList, aspectInfoList);
 
     pending.remove(path);
     env.mutability().freeze();
@@ -518,12 +548,16 @@
       BuildFileAST buildFileAST,
       Map<String, Extension> imports,
       List<RuleInfoWrapper> ruleInfoList,
-      List<ProviderInfoWrapper> providerInfoList)
+      List<ProviderInfoWrapper> providerInfoList,
+      List<AspectInfoWrapper> aspectInfoList)
       throws InterruptedException, StarlarkEvaluationException {
 
     Environment env =
         createEnvironment(
-            semantics, eventHandler, globalFrame(ruleInfoList, providerInfoList), imports);
+            semantics,
+            eventHandler,
+            globalFrame(ruleInfoList, providerInfoList, aspectInfoList),
+            imports);
 
     if (!buildFileAST.exec(env, eventHandler)) {
       throw new StarlarkEvaluationException("Starlark evaluation error");
@@ -543,14 +577,16 @@
    *     invocation information will be added
    */
   private static GlobalFrame globalFrame(
-      List<RuleInfoWrapper> ruleInfoList, List<ProviderInfoWrapper> providerInfoList) {
+      List<RuleInfoWrapper> ruleInfoList,
+      List<ProviderInfoWrapper> providerInfoList,
+      List<AspectInfoWrapper> aspectInfoList) {
     TopLevelBootstrap topLevelBootstrap =
         new TopLevelBootstrap(
             new FakeBuildApiGlobals(),
             new FakeSkylarkAttrApi(),
             new FakeSkylarkCommandLineApi(),
             new FakeSkylarkNativeModuleApi(),
-            new FakeSkylarkRuleFunctionsApi(ruleInfoList, providerInfoList),
+            new FakeSkylarkRuleFunctionsApi(ruleInfoList, providerInfoList, aspectInfoList),
             new FakeStructProviderApi(),
             new FakeOutputGroupInfoProvider(),
             new FakeActionsInfoProvider(),
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkAspect.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkAspect.java
index 7cd6e10..c24273b 100644
--- a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkAspect.java
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkAspect.java
@@ -16,11 +16,22 @@
 
 import com.google.devtools.build.lib.skylarkbuildapi.SkylarkAspectApi;
 import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
+import com.google.devtools.build.lib.syntax.BaseFunction;
+import com.google.devtools.build.lib.syntax.FunctionSignature;
 
-/**
- * Fake implementation of {@link SkylarkAspectApi}.
- */
-public class FakeSkylarkAspect implements SkylarkAspectApi {
+/** Fake implementation of {@link SkylarkAspectApi}. */
+public class FakeSkylarkAspect extends BaseFunction implements SkylarkAspectApi {
+
+  /**
+   * Each fake is constructed with a unique name, controlled by this counter being the name suffix.
+   */
+  private static int idCounter = 0;
+
+  public FakeSkylarkAspect() {
+    super(
+        "AspectIdentifier" + idCounter++,
+        FunctionSignature.WithValues.create(FunctionSignature.KWARGS));
+  }
 
   @Override
   public void repr(SkylarkPrinter printer) {}
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 fa82b8a..c30ab1e 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
@@ -32,13 +32,16 @@
 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.rendering.AspectInfoWrapper;
 import com.google.devtools.build.skydoc.rendering.ProviderInfoWrapper;
 import com.google.devtools.build.skydoc.rendering.RuleInfoWrapper;
+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.ProviderFieldInfo;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ProviderInfo;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.RuleInfo;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
 import java.util.List;
@@ -60,6 +63,8 @@
 
   private final List<ProviderInfoWrapper> providerInfoList;
 
+  private final List<AspectInfoWrapper> aspectInfoList;
+
   /**
    * Constructor.
    *
@@ -67,11 +72,16 @@
    *     will be added
    * @param providerInfoList the list of {@link ProviderInfo} objects to which provider() invocation
    *     information will be added
+   * @param aspectInfoList the list of {@link AspectInfo} objects to which aspect() invocation
+   *     information will be added
    */
   public FakeSkylarkRuleFunctionsApi(
-      List<RuleInfoWrapper> ruleInfoList, List<ProviderInfoWrapper> providerInfoList) {
+      List<RuleInfoWrapper> ruleInfoList,
+      List<ProviderInfoWrapper> providerInfoList,
+      List<AspectInfoWrapper> aspectInfoList) {
     this.ruleInfoList = ruleInfoList;
     this.providerInfoList = providerInfoList;
+    this.aspectInfoList = aspectInfoList;
   }
 
   @Override
@@ -138,7 +148,6 @@
       Environment funcallEnv,
       StarlarkContext context)
       throws EvalException {
-    List<AttributeInfo> attrInfos;
     ImmutableMap.Builder<String, FakeDescriptor> attrsMapBuilder = ImmutableMap.builder();
     if (attrs != null && attrs != Runtime.NONE) {
       SkylarkDict<?, ?> attrsDict = (SkylarkDict<?, ?>) attrs;
@@ -146,7 +155,7 @@
     }
 
     attrsMapBuilder.put("name", IMPLICIT_NAME_ATTRIBUTE_DESCRIPTOR);
-    attrInfos =
+    List<AttributeInfo> attrInfos =
         attrsMapBuilder.build().entrySet().stream()
             .filter(entry -> !entry.getKey().startsWith("_"))
             .map(entry -> entry.getValue().asAttributeInfo(entry.getKey()))
@@ -186,7 +195,39 @@
       Object attrs, SkylarkList<?> requiredAspectProvidersArg, SkylarkList<?> providesArg,
       SkylarkList<?> fragments, SkylarkList<?> hostFragments, SkylarkList<?> toolchains, String doc,
       FuncallExpression ast, Environment funcallEnv) throws EvalException {
-    return new FakeSkylarkAspect();
+    FakeSkylarkAspect fakeAspect = new FakeSkylarkAspect();
+    ImmutableMap.Builder<String, FakeDescriptor> attrsMapBuilder = ImmutableMap.builder();
+    if (attrs != null && attrs != Runtime.NONE) {
+      SkylarkDict<?, ?> attrsDict = (SkylarkDict<?, ?>) attrs;
+      attrsMapBuilder.putAll(attrsDict.getContents(String.class, FakeDescriptor.class, "attrs"));
+    }
+
+    attrsMapBuilder.put("name", IMPLICIT_NAME_ATTRIBUTE_DESCRIPTOR);
+    List<AttributeInfo> attrInfos =
+        attrsMapBuilder.build().entrySet().stream()
+            .filter(entry -> !entry.getKey().startsWith("_"))
+            .map(entry -> entry.getValue().asAttributeInfo(entry.getKey()))
+            .collect(Collectors.toList());
+    attrInfos.sort(new AttributeNameComparator());
+
+    List<String> aspectAttrs = new ArrayList<>();
+    if (attributeAspects != null) {
+      aspectAttrs = attributeAspects.getContents(String.class, "aspectAttrs");
+    }
+
+    aspectAttrs =
+        aspectAttrs.stream().filter(entry -> !entry.startsWith("_")).collect(Collectors.toList());
+
+    // Only the Builder is passed to AspectInfoWrapper as the aspect name is not yet available.
+    AspectInfo.Builder aspectInfo =
+        AspectInfo.newBuilder()
+            .setDocString(doc)
+            .addAllAttribute(attrInfos)
+            .addAllAspectAttribute(aspectAttrs);
+
+    aspectInfoList.add(new AspectInfoWrapper(fakeAspect, ast.getLocation(), aspectInfo));
+
+    return fakeAspect;
   }
 
   /**
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/AspectInfoWrapper.java b/src/main/java/com/google/devtools/build/skydoc/rendering/AspectInfoWrapper.java
new file mode 100644
index 0000000..95d43e1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/AspectInfoWrapper.java
@@ -0,0 +1,46 @@
+// Copyright 2019 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.events.Location;
+import com.google.devtools.build.lib.syntax.BaseFunction;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AspectInfo;
+
+/** Stores information about a skylark aspect definition. */
+public class AspectInfoWrapper {
+  private final BaseFunction identifierFunction;
+  private final Location location;
+  // Only the Builder is passed to AspectInfoWrapper as the aspect name is not yet available.
+  private final AspectInfo.Builder aspectInfo;
+
+  public AspectInfoWrapper(
+      BaseFunction identifierFunction, Location location, AspectInfo.Builder aspectInfo) {
+    this.identifierFunction = identifierFunction;
+    this.location = location;
+    this.aspectInfo = aspectInfo;
+  }
+
+  public BaseFunction getIdentifierFunction() {
+    return identifierFunction;
+  }
+
+  public Location getLocation() {
+    return location;
+  }
+
+  public AspectInfo.Builder getAspectInfo() {
+    return aspectInfo;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/ProtoRenderer.java b/src/main/java/com/google/devtools/build/skydoc/rendering/ProtoRenderer.java
index 5048724..475b701 100644
--- a/src/main/java/com/google/devtools/build/skydoc/rendering/ProtoRenderer.java
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/ProtoRenderer.java
@@ -15,6 +15,7 @@
 package com.google.devtools.build.skydoc.rendering;
 
 import com.google.devtools.build.lib.syntax.UserDefinedFunction;
+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;
@@ -66,6 +67,14 @@
     build.writeTo(outputStream);
   }
 
+  /** Appends {@link AspectInfo} protos to a {@link ModuleInfo.Builder}. */
+  public ProtoRenderer appendAspectInfos(Collection<AspectInfo> aspectInfos) {
+    for (AspectInfo aspectInfo : aspectInfos) {
+      moduleInfo.addAspectInfo(aspectInfo);
+    }
+    return this;
+  }
+
   public ModuleInfo.Builder getModuleInfo() {
     return moduleInfo;
   }
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 9fc3be7..d8850bf 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
@@ -32,6 +32,8 @@
   repeated ProviderInfo provider_info = 2;
 
   repeated UserDefinedFunctionInfo func_info = 3;
+
+  repeated AspectInfo aspect_info = 4;
 }
 
 // Representation of a Starlark rule attribute type. These generally
@@ -141,3 +143,18 @@
   // The fields of the provider.
   repeated ProviderFieldInfo field_info = 3;
 }
+
+// Representation of a Starlark aspect definition.
+message AspectInfo {
+  // The name of the aspect.
+  string aspect_name = 1;
+
+  // The documentation string of the aspect.
+  string doc_string = 2;
+
+  // The rule attributes along which the aspect propagates.
+  repeated string aspect_attribute = 3;
+
+  // The attributes of the aspect.
+  repeated AttributeInfo attribute = 4;
+}
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 9917456..29ca936 100644
--- a/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java
+++ b/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java
@@ -31,6 +31,7 @@
 import com.google.devtools.build.skydoc.rendering.DocstringParseException;
 import com.google.devtools.build.skydoc.rendering.FunctionUtil;
 import com.google.devtools.build.skydoc.rendering.ProtoRenderer;
+import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AspectInfo;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AttributeType;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ModuleInfo;
 import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ProviderInfo;
@@ -94,6 +95,7 @@
                     Label.parseAbsoluteUnchecked("//test:test.bzl"),
                     ImmutableMap.builder(),
                     ImmutableMap.builder(),
+                    ImmutableMap.builder(),
                     ImmutableMap.builder()));
 
     assertThat(expected).hasMessageThat().contains("Starlark evaluation error");
@@ -124,6 +126,7 @@
         Label.parseAbsoluteUnchecked("//test:test.bzl"),
         ruleInfoMap,
         ImmutableMap.builder(),
+        ImmutableMap.builder(),
         ImmutableMap.builder());
     Map<String, RuleInfo> ruleInfos = ruleInfoMap.build();
     assertThat(ruleInfos).hasSize(1);
@@ -148,12 +151,24 @@
         .collect(Collectors.toList());
   }
 
+  private static Iterable<String> getAttrNames(AspectInfo aspectInfo) {
+    return aspectInfo.getAttributeList().stream()
+        .map(attr -> attr.getName())
+        .collect(Collectors.toList());
+  }
+
   private static Iterable<AttributeType> getAttrTypes(RuleInfo ruleInfo) {
     return ruleInfo.getAttributeList().stream()
         .map(attr -> attr.getType())
         .collect(Collectors.toList());
   }
 
+  private static Iterable<AttributeType> getAttrTypes(AspectInfo aspectInfo) {
+    return aspectInfo.getAttributeList().stream()
+        .map(attr -> attr.getType())
+        .collect(Collectors.toList());
+  }
+
   @Test
   public void testMultipleRuleNames() throws Exception {
     scratch.file(
@@ -188,6 +203,7 @@
         Label.parseAbsoluteUnchecked("//test:test.bzl"),
         ruleInfoMap,
         ImmutableMap.builder(),
+        ImmutableMap.builder(),
         ImmutableMap.builder());
 
     assertThat(ruleInfoMap.build().keySet()).containsExactly("rule_one", "rule_two");
@@ -231,6 +247,7 @@
         Label.parseAbsoluteUnchecked("//test:main.bzl"),
         ruleInfoMapBuilder,
         ImmutableMap.builder(),
+        ImmutableMap.builder(),
         ImmutableMap.builder());
 
     Map<String, RuleInfo> ruleInfoMap = ruleInfoMapBuilder.build();
@@ -279,6 +296,7 @@
         Label.parseAbsoluteUnchecked("//test:main.bzl"),
         ruleInfoMapBuilder,
         ImmutableMap.builder(),
+        ImmutableMap.builder(),
         ImmutableMap.builder());
 
     Map<String, RuleInfo> ruleInfoMap = ruleInfoMapBuilder.build();
@@ -307,6 +325,7 @@
         Label.parseAbsoluteUnchecked("//test:main.bzl"),
         ruleInfoMapBuilder,
         ImmutableMap.builder(),
+        ImmutableMap.builder(),
         ImmutableMap.builder());
 
     Map<String, RuleInfo> ruleInfoMap = ruleInfoMapBuilder.build();
@@ -343,6 +362,7 @@
                     Label.parseAbsoluteUnchecked("//test:main.bzl"),
                     ImmutableMap.builder(),
                     ImmutableMap.builder(),
+                    ImmutableMap.builder(),
                     ImmutableMap.builder()));
 
     assertThat(expected).hasMessageThat().contains("cycle with test/main.bzl");
@@ -375,7 +395,8 @@
         Label.parseAbsoluteUnchecked("//test:main.bzl"),
         ImmutableMap.builder(),
         ImmutableMap.builder(),
-        functionInfoBuilder);
+        functionInfoBuilder,
+        ImmutableMap.builder());
 
     UserDefinedFunction checkSourcesFn = functionInfoBuilder.build().get("check_sources");
     DocstringParseException expected =
@@ -417,7 +438,8 @@
         Label.parseAbsoluteUnchecked("//test:test.bzl"),
         ImmutableMap.builder(),
         ImmutableMap.builder(),
-        funcInfoMap);
+        funcInfoMap,
+        ImmutableMap.builder());
 
     Map<String, UserDefinedFunction> functions = funcInfoMap.build();
     assertThat(functions).hasSize(1);
@@ -455,6 +477,7 @@
         Label.parseAbsoluteUnchecked("//test:test.bzl"),
         ImmutableMap.builder(),
         providerInfoMap,
+        ImmutableMap.builder(),
         ImmutableMap.builder());
 
     Map<String, ProviderInfo> providers = providerInfoMap.build();
@@ -482,4 +505,46 @@
         .map(field -> field.getDocString())
         .collect(Collectors.toList());
   }
+
+  @Test
+  public void testAspectInfo() throws Exception {
+    scratch.file(
+        "/test/test.bzl",
+        "def my_aspect_impl(ctx):\n"
+            + "    return []\n"
+            + "\n"
+            + "my_aspect = aspect(\n"
+            + "    implementation = my_aspect_impl,\n"
+            + "    doc = \"This is my aspect. It does stuff.\",\n"
+            + "    attr_aspects = [\"deps\"],\n"
+            + "    attrs = {\n"
+            + "        \"first\": attr.label(mandatory = True, allow_single_file = True),\n"
+            + "        \"second\": attr.string_dict(mandatory = True),\n"
+            + "        \"_third\": attr.label(mandatory = True, allow_single_file = True),\n"
+            + "    },\n"
+            + ")");
+
+    ImmutableMap.Builder<String, AspectInfo> aspectInfoMap = ImmutableMap.builder();
+
+    skydocMain.eval(
+        StarlarkSemantics.DEFAULT_SEMANTICS,
+        Label.parseAbsoluteUnchecked("//test:test.bzl"),
+        ImmutableMap.builder(),
+        ImmutableMap.builder(),
+        ImmutableMap.builder(),
+        aspectInfoMap);
+    Map<String, AspectInfo> aspectInfos = aspectInfoMap.build();
+    assertThat(aspectInfos).hasSize(1);
+
+    ModuleInfo moduleInfo =
+        new ProtoRenderer().appendAspectInfos(aspectInfos.values()).getModuleInfo().build();
+    AspectInfo aspectInfo = moduleInfo.getAspectInfo(0);
+    assertThat(aspectInfo.getAspectName()).isEqualTo("my_aspect");
+    assertThat(aspectInfo.getDocString()).isEqualTo("This is my aspect. It does stuff.");
+    assertThat(getAttrNames(aspectInfo)).containsExactly("name", "first", "second").inOrder();
+    assertThat(getAttrTypes(aspectInfo))
+        .containsExactly(AttributeType.NAME, AttributeType.LABEL, AttributeType.STRING_DICT)
+        .inOrder();
+    assertThat(aspectInfo.getAspectAttributeList()).containsExactly("deps");
+  }
 }