Initial check-in of skydoc rewrite.

This skydoc rewrite uses an actual skylark interpreter with a faked build API (implementing the actual build API that Bazel uses).

There's a lot left to do here, this is a barebones start.
For example, this does not yet handle:
  - load() statements
  - non-global build API elements (e.g. apple_common)
  - output of any rule information other than attribute names
  - markdown output format

RELNOTES: None.
PiperOrigin-RevId: 202187207
diff --git a/src/BUILD b/src/BUILD
index c6f8137..7062b4d 100644
--- a/src/BUILD
+++ b/src/BUILD
@@ -324,6 +324,7 @@
         "//src/main/cpp:srcs",
         "//src/main/java/com/google/devtools/build/docgen:srcs",
         "//src/main/java/com/google/devtools/build/lib:srcs",
+        "//src/main/java/com/google/devtools/build/skydoc:srcs",
         "//src/main/java/com/google/devtools/build/skyframe:srcs",
         "//src/main/java/com/google/devtools/common/options:srcs",
         "//src/main/java/com/google/devtools/skylark:srcs",
@@ -340,6 +341,7 @@
         "//src/test/java/com/google/devtools/build/docgen:srcs",
         "//src/test/java/com/google/devtools/build/lib:srcs",
         "//src/test/java/com/google/devtools/build/lib/shell:srcs",
+        "//src/test/java/com/google/devtools/build/skydoc:srcs",
         "//src/test/java/com/google/devtools/build/skyframe:srcs",
         "//src/test/java/com/google/devtools/common/options:srcs",
         "//src/test/py/bazel:srcs",
diff --git a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java
index e85915f..85e3b97 100644
--- a/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java
+++ b/src/main/java/com/google/devtools/build/lib/analysis/skylark/SkylarkRuleClassFunctions.java
@@ -415,12 +415,12 @@
   @Override
   public SkylarkAspect aspect(
       BaseFunction implementation,
-      SkylarkList attributeAspects,
+      SkylarkList<?> attributeAspects,
       Object attrs,
-      SkylarkList requiredAspectProvidersArg,
-      SkylarkList providesArg,
-      SkylarkList fragments,
-      SkylarkList hostFragments,
+      SkylarkList<?> requiredAspectProvidersArg,
+      SkylarkList<?> providesArg,
+      SkylarkList<?> fragments,
+      SkylarkList<?> hostFragments,
       SkylarkList<?> toolchains,
       String doc,
       FuncallExpression ast,
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/SkylarkAttrApi.java b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/SkylarkAttrApi.java
index d9e0beb..edc3fb8 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/SkylarkAttrApi.java
+++ b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/SkylarkAttrApi.java
@@ -792,7 +792,7 @@
       useEnvironment = true)
   public Descriptor outputListAttribute(
       Boolean allowEmpty,
-      SkylarkList defaultList,
+      SkylarkList<?> defaultList,
       String doc,
       Boolean mandatory,
       Boolean nonEmpty,
diff --git a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/SkylarkRuleFunctionsApi.java b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/SkylarkRuleFunctionsApi.java
index 1f7a7e0..84abfe1 100644
--- a/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/SkylarkRuleFunctionsApi.java
+++ b/src/main/java/com/google/devtools/build/lib/skylarkbuildapi/SkylarkRuleFunctionsApi.java
@@ -429,12 +429,12 @@
   )
   public SkylarkAspectApi aspect(
       BaseFunction implementation,
-      SkylarkList attributeAspects,
+      SkylarkList<?> attributeAspects,
       Object attrs,
-      SkylarkList requiredAspectProvidersArg,
-      SkylarkList providesArg,
-      SkylarkList fragments,
-      SkylarkList hostFragments,
+      SkylarkList<?> requiredAspectProvidersArg,
+      SkylarkList<?> providesArg,
+      SkylarkList<?> fragments,
+      SkylarkList<?> hostFragments,
       SkylarkList<?> toolchains,
       String doc,
       FuncallExpression ast,
diff --git a/src/main/java/com/google/devtools/build/skydoc/BUILD b/src/main/java/com/google/devtools/build/skydoc/BUILD
new file mode 100644
index 0000000..d3bcf4b
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/BUILD
@@ -0,0 +1,44 @@
+# Description:
+#   Skydoc is a documentation generator for skylark files.
+#
+#   It is currently experimental and in development.
+#
+# Usage:
+# skydoc <target_file> <output_file>
+
+package(
+    default_visibility = ["//src:__subpackages__"],
+)
+
+licenses(["notice"])  # Apache 2.0
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]) + [
+        "//src/main/java/com/google/devtools/build/skydoc/fakebuildapi:srcs",
+        "//src/main/java/com/google/devtools/build/skydoc/rendering:srcs",
+    ],
+)
+
+java_binary(
+    name = "skydoc",
+    main_class = "com.google.devtools.build.skydoc.SkydocMain",
+    runtime_deps = [
+        ":skydoc_lib",
+    ],
+)
+
+java_library(
+    name = "skydoc_lib",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//src/main/java/com/google/devtools/build/lib:events",
+        "//src/main/java/com/google/devtools/build/lib:syntax",
+        "//src/main/java/com/google/devtools/build/lib/skylarkbuildapi",
+        "//src/main/java/com/google/devtools/build/lib/vfs:pathfragment",
+        "//src/main/java/com/google/devtools/build/skydoc/fakebuildapi",
+        "//src/main/java/com/google/devtools/build/skydoc/rendering",
+        "//third_party:guava",
+        "//third_party:jsr305",
+    ],
+)
diff --git a/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java b/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
new file mode 100644
index 0000000..1e14cc1
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
@@ -0,0 +1,190 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.skydoc;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.events.EventHandler;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.skylarkbuildapi.TopLevelBootstrap;
+import com.google.devtools.build.lib.syntax.BuildFileAST;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.Environment.Extension;
+import com.google.devtools.build.lib.syntax.Environment.GlobalFrame;
+import com.google.devtools.build.lib.syntax.MethodLibrary;
+import com.google.devtools.build.lib.syntax.Mutability;
+import com.google.devtools.build.lib.syntax.ParserInputSource;
+import com.google.devtools.build.lib.syntax.Runtime;
+import com.google.devtools.build.lib.vfs.PathFragment;
+import com.google.devtools.build.skydoc.fakebuildapi.FakeActionsInfoProvider;
+import com.google.devtools.build.skydoc.fakebuildapi.FakeBuildApiGlobals;
+import com.google.devtools.build.skydoc.fakebuildapi.FakeDefaultInfoProvider;
+import com.google.devtools.build.skydoc.fakebuildapi.FakeOutputGroupInfo.FakeOutputGroupInfoProvider;
+import com.google.devtools.build.skydoc.fakebuildapi.FakeSkylarkAttrApi;
+import com.google.devtools.build.skydoc.fakebuildapi.FakeSkylarkCommandLineApi;
+import com.google.devtools.build.skydoc.fakebuildapi.FakeSkylarkNativeModuleApi;
+import com.google.devtools.build.skydoc.fakebuildapi.FakeSkylarkRuleFunctionsApi;
+import com.google.devtools.build.skydoc.fakebuildapi.FakeStructApi.FakeStructProviderApi;
+import com.google.devtools.build.skydoc.rendering.RuleInfo;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Main entry point for the Skydoc binary.
+ *
+ * <p>Skydoc generates human-readable documentation for relevant details of skylark files by
+ * running a skylark interpreter with a fake implementation of the build API.</p>
+ *
+ * <p>Currently, Skydoc generates documentation for skylark rule definitions (discovered by
+ * invocations of the build API function {@code rule()}.</p>
+ *
+ * <p>Usage:</p>
+ * <pre>
+ *   skydoc {target_skylark_file} {output_file}
+ * </pre>
+ */
+public class SkydocMain {
+
+  // Pattern to match the assignment of a variable to a rule definition
+  // For example, 'my_rule = rule(' will match and have 'my_rule' available as group(1).
+  private static final Pattern ruleDefinitionLinePattern =
+      Pattern.compile("([^\\s]+) = rule\\(");
+
+  private final EventHandler eventHandler = new SystemOutEventHandler();
+
+  public static void main(String[] args) throws IOException, InterruptedException {
+    if (args.length != 2) {
+      throw new IllegalArgumentException("Expected two arguments. Usage:\n"
+          + "{skydoc_bin} {target_skylark_file} {output_file}");
+    }
+
+    String bzlPath = args[0];
+    String outputPath = args[1];
+
+    Path path = Paths.get(bzlPath);
+    byte[] content = Files.readAllBytes(path);
+
+    ParserInputSource parserInputSource =
+        ParserInputSource.create(content, PathFragment.create(path.toString()));
+
+    List<RuleInfo> ruleInfoList = new SkydocMain().eval(parserInputSource);
+
+    try (PrintWriter printWriter = new PrintWriter(outputPath, "UTF-8")) {
+      printRuleInfos(printWriter, ruleInfoList);
+    }
+  }
+
+  // TODO(cparsons): Improve output (markdown or HTML).
+  private static void printRuleInfos(
+      PrintWriter printWriter, List<RuleInfo> ruleInfos) throws IOException {
+    for (RuleInfo ruleInfo : ruleInfos) {
+      Location location = ruleInfo.getLocation();
+      Path filePath = Paths.get(location.getPath().getPathString());
+      List<String> lines = Files.readAllLines(filePath, UTF_8);
+      String definingString = lines.get(location.getStartLine() - 1);
+      // Rule definitions don't specify their own visible name directly. Instead, the name of
+      // a rule is dependent on the name of the variable assigend to the return value of rule().
+      // This attempts to find a line of the form 'foo = rule(' and thus label the rule as
+      // named 'foo'.
+      // TODO(cparsons): Inspect the global bindings of the environment instead of using string
+      // matching.
+      Matcher matcher = ruleDefinitionLinePattern.matcher(definingString);
+      if (matcher.matches()) {
+        printWriter.println(matcher.group(1));
+      } else {
+        printWriter.println("<unknown name>");
+      }
+      printWriter.println(ruleInfo.getDescription());
+    }
+  }
+
+  /**
+   * Evaluates/interprets the skylark file at the given input source using a fake build API and
+   * collects information about all rule definitions made in that file.
+   *
+   * @param parserInputSource the input source representing the input skylark file
+   * @return a list of {@link RuleInfo} objects describing the rule definitions
+   * @throws InterruptedException if evaluation is interrupted
+   */
+  // TODO(cparsons): Evaluate load statements recursively.
+  public List<RuleInfo> eval(ParserInputSource parserInputSource)
+      throws InterruptedException {
+    List<RuleInfo> ruleInfoList = new ArrayList<>();
+
+    BuildFileAST buildFileAST = BuildFileAST.parseSkylarkFile(
+        parserInputSource, eventHandler);
+
+    Environment env = createEnvironment(
+        eventHandler,
+        globalFrame(ruleInfoList),
+        /* imports= */ ImmutableMap.of());
+
+    if (!buildFileAST.exec(env, eventHandler)) {
+      throw new RuntimeException("Error loading file");
+    }
+
+    env.mutability().freeze();
+
+    return ruleInfoList;
+  }
+
+  /**
+   * Initialize and return a global frame containing the fake build API.
+   *
+   * @param ruleInfoList the list of {@link RuleInfo} objects, to which rule() invocation
+   *     information will be added
+   */
+  private static GlobalFrame globalFrame(List<RuleInfo> ruleInfoList) {
+    // TODO(cparsons): Complete the Fake Build API stubs. For example, implement provider(),
+    // and include the other bootstraps.
+    TopLevelBootstrap topLevelBootstrap =
+        new TopLevelBootstrap(new FakeBuildApiGlobals(),
+            new FakeSkylarkAttrApi(),
+            new FakeSkylarkCommandLineApi(),
+            new FakeSkylarkNativeModuleApi(),
+            new FakeSkylarkRuleFunctionsApi(ruleInfoList),
+            new FakeStructProviderApi(),
+            new FakeOutputGroupInfoProvider(),
+            new FakeActionsInfoProvider(),
+            new FakeDefaultInfoProvider());
+
+    ImmutableMap.Builder<String, Object> envBuilder = ImmutableMap.builder();
+
+    Runtime.addConstantsToBuilder(envBuilder);
+    MethodLibrary.addBindingsToBuilder(envBuilder);
+    topLevelBootstrap.addBindingsToBuilder(envBuilder);
+
+    return GlobalFrame.createForBuiltins(envBuilder.build());
+  }
+
+  private static Environment createEnvironment(EventHandler eventHandler, GlobalFrame globals,
+      Map<String, Extension> imports) {
+    return Environment.builder(Mutability.create("Skydoc"))
+        .useDefaultSemantics()
+        .setGlobals(globals)
+        .setImportedExtensions(imports)
+        .setEventHandler(eventHandler)
+        .build();
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/SystemOutEventHandler.java b/src/main/java/com/google/devtools/build/skydoc/SystemOutEventHandler.java
new file mode 100644
index 0000000..8d45627
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/SystemOutEventHandler.java
@@ -0,0 +1,50 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.skydoc;
+
+import com.google.devtools.build.lib.events.Event;
+import com.google.devtools.build.lib.events.EventHandler;
+
+/**
+ * A simple {@link EventHandler} which outputs log information to system.out and system.err.
+ */
+class SystemOutEventHandler implements EventHandler {
+
+  @Override
+  public void handle(Event event) {
+    switch (event.getKind()) {
+      case ERROR:
+      case WARNING:
+      case STDERR:
+        System.err.println(messageWithLocation(event));
+        break;
+      case DEBUG:
+      case INFO:
+      case PROGRESS:
+      case STDOUT:
+        System.out.println(messageWithLocation(event));
+        break;
+      default:
+        System.err.println("Unknown message type: " + event);
+    }
+  }
+
+  private String messageWithLocation(Event event) {
+    String location = event.getLocation() == null
+        ? "<no location>"
+        : event.getLocation().print();
+    return location + ": " + event.getMessage();
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/BUILD b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/BUILD
new file mode 100644
index 0000000..766f404
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/BUILD
@@ -0,0 +1,25 @@
+package(
+    default_visibility = ["//src:__subpackages__"],
+)
+
+licenses(["notice"])  # Apache 2.0
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]),
+)
+
+java_library(
+    name = "fakebuildapi",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//src/main/java/com/google/devtools/build/lib:events",
+        "//src/main/java/com/google/devtools/build/lib:skylarkinterface",
+        "//src/main/java/com/google/devtools/build/lib:syntax",
+        "//src/main/java/com/google/devtools/build/lib/cmdline",
+        "//src/main/java/com/google/devtools/build/lib/skylarkbuildapi",
+        "//src/main/java/com/google/devtools/build/skydoc/rendering",
+        "//third_party:guava",
+        "//third_party:jsr305",
+    ],
+)
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeActionsInfoProvider.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeActionsInfoProvider.java
new file mode 100644
index 0000000..b8643e3
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeActionsInfoProvider.java
@@ -0,0 +1,27 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.skydoc.fakebuildapi;
+
+import com.google.devtools.build.lib.skylarkbuildapi.ActionsInfoProviderApi;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
+
+/**
+ * Fake implementation of {@link ActionsInfoProviderApi}.
+ */
+public class FakeActionsInfoProvider implements ActionsInfoProviderApi {
+
+  @Override
+  public void repr(SkylarkPrinter printer) {}
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeBuildApiGlobals.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeBuildApiGlobals.java
new file mode 100644
index 0000000..d523070
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeBuildApiGlobals.java
@@ -0,0 +1,33 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.skydoc.fakebuildapi;
+
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.skylarkbuildapi.LateBoundDefaultApi;
+import com.google.devtools.build.lib.skylarkbuildapi.SkylarkBuildApiGlobals;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.EvalException;
+
+/**
+ * Fake implementation of {@link FakeBuildApiGlobals}.
+ */
+public class FakeBuildApiGlobals implements SkylarkBuildApiGlobals {
+
+  @Override
+  public LateBoundDefaultApi configurationField(String fragment, String name, Location loc,
+      Environment env) throws EvalException {
+    return null;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeDefaultInfoProvider.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeDefaultInfoProvider.java
new file mode 100644
index 0000000..1f5eacf
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeDefaultInfoProvider.java
@@ -0,0 +1,38 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.skydoc.fakebuildapi;
+
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.skylarkbuildapi.DefaultInfoApi;
+import com.google.devtools.build.lib.skylarkbuildapi.DefaultInfoApi.DefaultInfoApiProvider;
+import com.google.devtools.build.lib.skylarkbuildapi.FileApi;
+import com.google.devtools.build.lib.skylarkbuildapi.RunfilesApi;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
+import com.google.devtools.build.lib.syntax.EvalException;
+
+/**
+ * Fake implementation of {@link DefaultInfoApiProvider}.
+ */
+public class FakeDefaultInfoProvider implements DefaultInfoApiProvider<RunfilesApi, FileApi> {
+
+  @Override
+  public DefaultInfoApi constructor(Object files, Object runfiles, Object dataRunfiles,
+      Object defaultRunfiles, Object executable, Location loc) throws EvalException {
+    return null;
+  }
+
+  @Override
+  public void repr(SkylarkPrinter printer) {}
+}
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
new file mode 100644
index 0000000..77c912d
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeDescriptor.java
@@ -0,0 +1,30 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.skydoc.fakebuildapi;
+
+import com.google.devtools.build.lib.skylarkbuildapi.SkylarkAttrApi.Descriptor;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
+
+/**
+ * Fake implementation of {@link Descriptor}.
+ */
+public class FakeDescriptor implements Descriptor {
+
+  @Override
+  public void repr(SkylarkPrinter printer) {}
+
+  // TODO(cparsons): This class should store information about the attribute definition, for
+  // example, the attribute type.
+}
\ No newline at end of file
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeOutputGroupInfo.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeOutputGroupInfo.java
new file mode 100644
index 0000000..79b7014
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeOutputGroupInfo.java
@@ -0,0 +1,57 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.skydoc.fakebuildapi;
+
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.skylarkbuildapi.OutputGroupInfoApi;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.SkylarkDict;
+
+/**
+ * Fake implementation of {@link OutputGroupInfoApi}.
+ */
+public class FakeOutputGroupInfo implements OutputGroupInfoApi {
+
+  @Override
+  public String toProto(Location loc) throws EvalException {
+    return "";
+  }
+
+  @Override
+  public String toJson(Location loc) throws EvalException {
+    return "";
+  }
+
+  @Override
+  public void repr(SkylarkPrinter printer) {
+
+  }
+
+  /**
+   * Fake implementation of {@link OutputGroupInfoApiProvider}.
+   */
+  public static class FakeOutputGroupInfoProvider implements OutputGroupInfoApiProvider {
+
+    @Override
+    public OutputGroupInfoApi constructor(SkylarkDict<?, ?> kwargs, Location loc)
+        throws EvalException {
+      return new FakeOutputGroupInfo();
+    }
+
+    @Override
+    public void repr(SkylarkPrinter printer) {}
+  }
+}
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
new file mode 100644
index 0000000..f9e5f8c
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkAttrApi.java
@@ -0,0 +1,121 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.skydoc.fakebuildapi;
+
+import com.google.devtools.build.lib.skylarkbuildapi.SkylarkAttrApi;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.FuncallExpression;
+import com.google.devtools.build.lib.syntax.SkylarkDict;
+import com.google.devtools.build.lib.syntax.SkylarkList;
+
+/**
+ * Fake implementation of {@link SkylarkAttrApi}.
+ */
+public class FakeSkylarkAttrApi implements SkylarkAttrApi {
+
+  @Override
+  public Descriptor intAttribute(Integer defaultInt, String doc, Boolean mandatory,
+      SkylarkList<?> values, FuncallExpression ast, Environment env) throws EvalException {
+    return new FakeDescriptor();
+  }
+
+  @Override
+  public Descriptor stringAttribute(String defaultString, String doc, Boolean mandatory,
+      SkylarkList<?> values, FuncallExpression ast, Environment env) throws EvalException {
+    return new FakeDescriptor();
+  }
+
+  @Override
+  public Descriptor labelAttribute(Object defaultO, String doc, Boolean executable,
+      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();
+  }
+
+  @Override
+  public Descriptor stringListAttribute(Boolean mandatory, Boolean nonEmpty, Boolean allowEmpty,
+      SkylarkList<?> defaultList, String doc, FuncallExpression ast, Environment env)
+      throws EvalException {
+    return new FakeDescriptor();
+  }
+
+  @Override
+  public Descriptor intListAttribute(Boolean mandatory, Boolean nonEmpty, Boolean allowEmpty,
+      SkylarkList<?> defaultList, String doc, FuncallExpression ast, Environment env)
+      throws EvalException {
+    return new FakeDescriptor();
+  }
+
+  @Override
+  public Descriptor labelListAttribute(Boolean allowEmpty, Object defaultList, 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();
+  }
+
+  @Override
+  public Descriptor labelKeyedStringDictAttribute(Boolean allowEmpty, Object defaultList,
+      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();
+  }
+
+  @Override
+  public Descriptor boolAttribute(Boolean defaultO, String doc, Boolean mandatory,
+      FuncallExpression ast, Environment env) throws EvalException {
+    return new FakeDescriptor();
+  }
+
+  @Override
+  public Descriptor outputAttribute(Object defaultO, String doc, Boolean mandatory,
+      FuncallExpression ast, Environment env) throws EvalException {
+    return new FakeDescriptor();
+  }
+
+  @Override
+  public Descriptor outputListAttribute(Boolean allowEmpty, SkylarkList<?> defaultList, String doc,
+      Boolean mandatory, Boolean nonEmpty, FuncallExpression ast, Environment env)
+      throws EvalException {
+    return new FakeDescriptor();
+  }
+
+  @Override
+  public Descriptor stringDictAttribute(Boolean allowEmpty, SkylarkDict<?, ?> defaultO, String doc,
+      Boolean mandatory, Boolean nonEmpty, FuncallExpression ast, Environment env)
+      throws EvalException {
+    return new FakeDescriptor();
+  }
+
+  @Override
+  public Descriptor stringListDictAttribute(Boolean allowEmpty, SkylarkDict<?, ?> defaultO,
+      String doc, Boolean mandatory, Boolean nonEmpty, FuncallExpression ast, Environment env)
+      throws EvalException {
+    return new FakeDescriptor();
+  }
+
+  @Override
+  public Descriptor licenseAttribute(Object defaultO, String doc, Boolean mandatory,
+      FuncallExpression ast, Environment env) throws EvalException {
+    return new FakeDescriptor();
+  }
+
+  @Override
+  public void repr(SkylarkPrinter printer) {}
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkCommandLineApi.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkCommandLineApi.java
new file mode 100644
index 0000000..8c11461
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkCommandLineApi.java
@@ -0,0 +1,29 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.skydoc.fakebuildapi;
+
+import com.google.devtools.build.lib.skylarkbuildapi.SkylarkCommandLineApi;
+import com.google.devtools.build.lib.syntax.SkylarkNestedSet;
+
+/**
+ * Fake implementation of {@link SkylarkCommandLineApi}.
+ */
+public class FakeSkylarkCommandLineApi implements SkylarkCommandLineApi {
+
+  @Override
+  public String joinPaths(String separator, SkylarkNestedSet files) {
+    return "";
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkNativeModuleApi.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkNativeModuleApi.java
new file mode 100644
index 0000000..22fa94e
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkNativeModuleApi.java
@@ -0,0 +1,72 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.skydoc.fakebuildapi;
+
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.skylarkbuildapi.SkylarkNativeModuleApi;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.FuncallExpression;
+import com.google.devtools.build.lib.syntax.Runtime.NoneType;
+import com.google.devtools.build.lib.syntax.SkylarkDict;
+import com.google.devtools.build.lib.syntax.SkylarkList;
+import com.google.devtools.build.lib.syntax.SkylarkList.MutableList;
+
+/**
+ * Fake implementation of {@link SkylarkNativeModuleApi}.
+ */
+public class FakeSkylarkNativeModuleApi implements SkylarkNativeModuleApi {
+
+  @Override
+  public SkylarkList<?> glob(SkylarkList<?> include, SkylarkList<?> exclude,
+      Integer excludeDirectories, FuncallExpression ast, Environment env)
+      throws EvalException, InterruptedException {
+    return MutableList.of(env);
+  }
+
+  @Override
+  public Object existingRule(String name, FuncallExpression ast, Environment env)
+      throws EvalException, InterruptedException {
+    return null;
+  }
+
+  @Override
+  public SkylarkDict<String, SkylarkDict<String, Object>> existingRules(FuncallExpression ast,
+      Environment env) throws EvalException, InterruptedException {
+    return SkylarkDict.of(env);
+  }
+
+  @Override
+  public NoneType packageGroup(String name, SkylarkList<?> packages, SkylarkList<?> includes,
+      FuncallExpression ast, Environment env) throws EvalException {
+    return null;
+  }
+
+  @Override
+  public NoneType exportsFiles(SkylarkList<?> srcs, Object visibility, Object licenses,
+      FuncallExpression ast, Environment env) throws EvalException {
+    return null;
+  }
+
+  @Override
+  public String packageName(FuncallExpression ast, Environment env) throws EvalException {
+    return "";
+  }
+
+  @Override
+  public String repositoryName(Location location, Environment env) throws EvalException {
+    return "";
+  }
+}
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
new file mode 100644
index 0000000..dcae202
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeSkylarkRuleFunctionsApi.java
@@ -0,0 +1,103 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.skydoc.fakebuildapi;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.devtools.build.lib.cmdline.Label;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.skylarkbuildapi.FileApi;
+import com.google.devtools.build.lib.skylarkbuildapi.FileTypeApi;
+import com.google.devtools.build.lib.skylarkbuildapi.ProviderApi;
+import com.google.devtools.build.lib.skylarkbuildapi.SkylarkAspectApi;
+import com.google.devtools.build.lib.skylarkbuildapi.SkylarkAttrApi.Descriptor;
+import com.google.devtools.build.lib.skylarkbuildapi.SkylarkRuleFunctionsApi;
+import com.google.devtools.build.lib.syntax.BaseFunction;
+import com.google.devtools.build.lib.syntax.Environment;
+import com.google.devtools.build.lib.syntax.EvalException;
+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.rendering.RuleInfo;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Fake implementation of {@link SkylarkRuleFunctionsApi}.
+ *
+ * <p>This fake hooks into the global {@code rule()} function, noting calls of that function
+ * with a {@link RuleInfoCollector} given in the class constructor.</p>
+ */
+public class FakeSkylarkRuleFunctionsApi implements SkylarkRuleFunctionsApi<FileApi> {
+
+  private final List<RuleInfo> ruleInfoList;
+
+  /**
+   * Constructor.
+   *
+   * @param ruleInfoList the list of {@link RuleInfo} objects to which rule() invocation information
+   *     will be added
+   */
+  public FakeSkylarkRuleFunctionsApi(List<RuleInfo> ruleInfoList) {
+    this.ruleInfoList = ruleInfoList;
+  }
+
+  @Override
+  public ProviderApi provider(String doc, Object fields, Location location) throws EvalException {
+    return null;
+  }
+
+  @Override
+  public BaseFunction rule(BaseFunction implementation, Boolean test, Object attrs,
+      Object implicitOutputs, Boolean executable, Boolean outputToGenfiles,
+      SkylarkList<?> fragments, SkylarkList<?> hostFragments, Boolean skylarkTestable,
+      SkylarkList<?> toolchains, String doc, SkylarkList<?> providesArg,
+      Boolean executionPlatformConstraintsAllowed, SkylarkList<?> execCompatibleWith,
+      FuncallExpression ast, Environment funcallEnv) throws EvalException {
+    Set<String> attrNames;
+    if (attrs != null) {
+      SkylarkDict<?, ?> attrsDict = (SkylarkDict<?, ?>) attrs;
+      Map<String, Descriptor> attrsMap =
+          attrsDict.getContents(String.class, Descriptor.class, "attrs");
+      attrNames = attrsMap.keySet();
+    } else {
+      attrNames = ImmutableSet.of();
+    }
+
+    // TODO(cparsons): Improve details given to RuleInfo (for example, attribute types).
+    ruleInfoList.add(new RuleInfo(ast.getLocation(), doc, attrNames));
+    return implementation;
+  }
+
+  @Override
+  public Label label(String labelString, Boolean relativeToCallerRepository, Location loc,
+      Environment env) throws EvalException {
+    return null;
+  }
+
+  @Override
+  public FileTypeApi<FileApi> fileType(SkylarkList<?> types, Location loc, Environment env)
+      throws EvalException {
+    return null;
+  }
+
+  @Override
+  public SkylarkAspectApi aspect(BaseFunction implementation, SkylarkList<?> attributeAspects,
+      Object attrs, SkylarkList<?> requiredAspectProvidersArg, SkylarkList<?> providesArg,
+      SkylarkList<?> fragments, SkylarkList<?> hostFragments, SkylarkList<?> toolchains, String doc,
+      FuncallExpression ast, Environment funcallEnv) throws EvalException {
+    return null;
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeStructApi.java b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeStructApi.java
new file mode 100644
index 0000000..abdea38
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/fakebuildapi/FakeStructApi.java
@@ -0,0 +1,88 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.skydoc.fakebuildapi;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.devtools.build.lib.events.Location;
+import com.google.devtools.build.lib.skylarkbuildapi.StructApi;
+import com.google.devtools.build.lib.skylarkinterface.SkylarkPrinter;
+import com.google.devtools.build.lib.syntax.ClassObject;
+import com.google.devtools.build.lib.syntax.EvalException;
+import com.google.devtools.build.lib.syntax.SkylarkDict;
+import java.util.Map;
+import javax.annotation.Nullable;
+
+/**
+ * Fake implementation of {@link StructApi}.
+ */
+public class FakeStructApi implements StructApi, ClassObject {
+
+  private final Map<String, Object> objects;
+
+  public FakeStructApi(Map<String, Object> objects) {
+    this.objects = objects;
+  }
+
+  public FakeStructApi() {
+    this(ImmutableMap.of());
+  }
+
+  @Override
+  public String toProto(Location loc) throws EvalException {
+    return "";
+  }
+
+  @Override
+  public String toJson(Location loc) throws EvalException {
+    return "";
+  }
+
+  @Override
+  public void repr(SkylarkPrinter printer) {}
+
+  @Nullable
+  @Override
+  public Object getValue(String name) throws EvalException {
+    return objects.get(name);
+  }
+
+  @Override
+  public ImmutableCollection<String> getFieldNames() throws EvalException {
+    return ImmutableList.of();
+  }
+
+  @Nullable
+  @Override
+  public String getErrorMessageForUnknownField(String field) {
+    return "";
+  }
+
+  /**
+   * Fake implementation of {@link StructProviderApi}.
+   */
+  public static class FakeStructProviderApi implements StructProviderApi {
+
+    @Override
+    public StructApi createStruct(SkylarkDict<?, ?> kwargs, Location loc) throws EvalException {
+      return new FakeStructApi(kwargs.getContents(String.class, Object.class, "kwargs"));
+    }
+
+    @Override
+    public void repr(SkylarkPrinter printer) {}
+  }
+}
+
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/BUILD b/src/main/java/com/google/devtools/build/skydoc/rendering/BUILD
new file mode 100644
index 0000000..13f2cc9
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/BUILD
@@ -0,0 +1,21 @@
+package(
+    default_visibility = ["//src:__subpackages__"],
+)
+
+licenses(["notice"])  # Apache 2.0
+
+filegroup(
+    name = "srcs",
+    srcs = glob(["**"]),
+)
+
+java_library(
+    name = "rendering",
+    srcs = glob(["*.java"]),
+    deps = [
+        "//src/main/java/com/google/devtools/build/lib:events",
+        "//src/main/java/com/google/devtools/build/lib:skylarkinterface",
+        "//third_party:guava",
+        "//third_party:jsr305",
+    ],
+)
diff --git a/src/main/java/com/google/devtools/build/skydoc/rendering/RuleInfo.java b/src/main/java/com/google/devtools/build/skydoc/rendering/RuleInfo.java
new file mode 100644
index 0000000..0d8a150
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/skydoc/rendering/RuleInfo.java
@@ -0,0 +1,58 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.skydoc.rendering;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.devtools.build.lib.events.Location;
+import java.util.Collection;
+
+/**
+ * Stores information about a skylark rule definition.
+ */
+public class RuleInfo {
+
+  private final Location location;
+  private final String docString;
+  private final Collection<String> attrNames;
+
+  public RuleInfo(Location location, String docString, Collection<String> attrNames) {
+    this.location = location;
+    this.docString = docString;
+    this.attrNames = attrNames;
+  }
+
+  public Location getLocation() {
+    return location;
+  }
+
+  public String getDocString() {
+    return docString;
+  }
+
+  public Collection<String> getAttrNames() {
+    return attrNames;
+  }
+
+  public String getDescription() {
+    StringBuilder stringBuilder = new StringBuilder();
+    if (!Strings.isNullOrEmpty(docString)) {
+      stringBuilder.append(docString);
+      stringBuilder.append("\n");
+    }
+    Joiner.on(",").appendTo(stringBuilder, attrNames);
+    return stringBuilder.toString();
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/skydoc/BUILD b/src/test/java/com/google/devtools/build/skydoc/BUILD
new file mode 100644
index 0000000..2416d5a
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/BUILD
@@ -0,0 +1,83 @@
+package(
+    default_testonly = 1,
+    default_visibility = ["//src:__subpackages__"],
+)
+
+filegroup(
+    name = "srcs",
+    testonly = 0,
+    srcs = glob(["**"]),
+    visibility = ["//src:__pkg__"],
+)
+
+java_test(
+    name = "SkydocTest",
+    size = "medium",
+    srcs = ["SkydocTest.java"],
+    shard_count = 1,
+    visibility = ["//devtools/blaze/main:__pkg__"],
+    deps = [
+        "//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/rendering",
+        "//src/test/java/com/google/devtools/build/lib:testutil",
+        "//src/test/java/com/google/devtools/build/lib/skylark:testutil",
+        "//third_party:guava",
+        "//third_party:junit4",
+        "//third_party:truth",
+    ],
+)
+
+# TODO(cparsons): Make test cases and golden-genrules into a macro.
+sh_test(
+    name = "simple_skydoc_e2e_test",
+    srcs = ["skydoc_e2e_test_runner.sh"],
+    args = [
+        "$(location //src/main/java/com/google/devtools/build/skydoc:skydoc)",
+        "$(location simple_test/input.txt)",
+        "$(location simple_test/golden.txt)",
+    ],
+    data = [
+        "simple_test/golden.txt",
+        "simple_test/input.txt",
+        "//src/main/java/com/google/devtools/build/skydoc",
+    ],
+)
+
+genrule(
+    name = "regenerate_simple_golden",
+    srcs = [
+        "simple_test/input.txt",
+    ],
+    outs = ["simple_output.txt"],
+    cmd = "$(location //src/main/java/com/google/devtools/build/skydoc:skydoc) " +
+          "$(location simple_test/input.txt) $(location simple_output.txt)",
+    tools = ["//src/main/java/com/google/devtools/build/skydoc"],
+)
+
+sh_test(
+    name = "unknown_name_skydoc_e2e_test",
+    srcs = ["skydoc_e2e_test_runner.sh"],
+    args = [
+        "$(location //src/main/java/com/google/devtools/build/skydoc:skydoc)",
+        "$(location unknown_name_test/input.txt)",
+        "$(location unknown_name_test/golden.txt)",
+    ],
+    data = [
+        "unknown_name_test/golden.txt",
+        "unknown_name_test/input.txt",
+        "//src/main/java/com/google/devtools/build/skydoc",
+    ],
+)
+
+genrule(
+    name = "regenerate_unknown_name_golden",
+    srcs = [
+        "unknown_name_test/input.txt",
+    ],
+    outs = ["unknown_name_output.txt"],
+    cmd = "$(location //src/main/java/com/google/devtools/build/skydoc:skydoc) " +
+          "$(location unknown_name_test/input.txt) $(location unknown_name_output.txt)",
+    tools = ["//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
new file mode 100644
index 0000000..d80fe4e
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/SkydocTest.java
@@ -0,0 +1,67 @@
+// Copyright 2018 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.devtools.build.skydoc;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.common.collect.Iterables;
+import com.google.devtools.build.lib.skylark.util.SkylarkTestCase;
+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.rendering.RuleInfo;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Java tests for Skydoc.
+ */
+@RunWith(JUnit4.class)
+public final class SkydocTest extends SkylarkTestCase {
+
+  @Test
+  public void testRuleInfoAttrs() throws Exception {
+    Path file =
+        scratch.file(
+            "/test/test.bzl",
+            "def rule_impl(ctx):",
+            "  return struct()",
+            "",
+            "my_rule = rule(",
+            "    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),",
+            "    },",
+            ")");
+    byte[] bytes = FileSystemUtils.readWithKnownFileSize(file, file.getFileSize());
+
+    ParserInputSource parserInputSource =
+        ParserInputSource.create(bytes, file.asFragment());
+
+    List<RuleInfo> ruleInfos = new SkydocMain().eval(parserInputSource);
+    assertThat(ruleInfos).hasSize(1);
+
+    RuleInfo ruleInfo = Iterables.getOnlyElement(ruleInfos);
+    assertThat(ruleInfo.getDocString()).isEqualTo("This is my rule. It does stuff.");
+    assertThat(ruleInfo.getAttrNames()).containsExactly(
+        "first", "second", "third", "fourth");
+  }
+}
diff --git a/src/test/java/com/google/devtools/build/skydoc/simple_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/simple_test/golden.txt
new file mode 100644
index 0000000..0bf3a8c
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/simple_test/golden.txt
@@ -0,0 +1,3 @@
+my_rule
+This is my rule. It does stuff.
+first,second,third,fourth
diff --git a/src/test/java/com/google/devtools/build/skydoc/simple_test/input.txt b/src/test/java/com/google/devtools/build/skydoc/simple_test/input.txt
new file mode 100644
index 0000000..de1548b
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/simple_test/input.txt
@@ -0,0 +1,13 @@
+def my_rule_impl(ctx):
+    return struct()
+
+my_rule = rule(
+    implementation = my_rule_impl,
+    doc = "This is my rule. It does stuff.",
+    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),
+    },
+)
diff --git a/src/test/java/com/google/devtools/build/skydoc/skydoc_e2e_test_runner.sh b/src/test/java/com/google/devtools/build/skydoc/skydoc_e2e_test_runner.sh
new file mode 100755
index 0000000..c32973c
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/skydoc_e2e_test_runner.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+#
+# Copyright 2018 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# Test skydoc output matches the expected golden file output.
+
+set -u
+
+skydoc_bin=$1
+input_file=$2
+golden_file=$3
+
+actual_file="${TEST_TMPDIR}/actual"
+
+${skydoc_bin} ${input_file} ${actual_file}
+
+DIFF="$(diff ${actual_file} ${golden_file})"
+
+if [ "$DIFF" != "" ]
+then
+    echo "Actual did not match golden."
+    echo "${DIFF}"
+    exit 1
+else
+    echo "Result matches golden file"
+fi
diff --git a/src/test/java/com/google/devtools/build/skydoc/unknown_name_test/golden.txt b/src/test/java/com/google/devtools/build/skydoc/unknown_name_test/golden.txt
new file mode 100644
index 0000000..9b49c77
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/unknown_name_test/golden.txt
@@ -0,0 +1,2 @@
+<unknown name>
+first,second,third,fourth
diff --git a/src/test/java/com/google/devtools/build/skydoc/unknown_name_test/input.txt b/src/test/java/com/google/devtools/build/skydoc/unknown_name_test/input.txt
new file mode 100644
index 0000000..24c69d2
--- /dev/null
+++ b/src/test/java/com/google/devtools/build/skydoc/unknown_name_test/input.txt
@@ -0,0 +1,12 @@
+def my_rule_impl(ctx):
+    return struct()
+
+rule(
+    implementation = my_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),
+    },
+)