Inspect post-evaluation exported variables to obtain rule names.

This is a much cleaner, more elegant approach than previous regex matching.
This still leaves room for unknown-name rule definitions, in case, for example, a user namespaces their rule definition not at the top level.
For example: "foo.bar = rule(...)"

RELNOTES: None.
PiperOrigin-RevId: 202380975
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 1e14cc1..1d9043f 100644
--- a/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
+++ b/src/main/java/com/google/devtools/build/skydoc/SkydocMain.java
@@ -14,12 +14,12 @@
 
 package com.google.devtools.build.skydoc;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-
+import com.google.common.base.Functions;
+import com.google.common.collect.ImmutableList;
 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.BaseFunction;
 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;
@@ -47,8 +47,8 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
 
 /**
  * Main entry point for the Skydoc binary.
@@ -66,11 +66,6 @@
  */
 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 {
@@ -88,35 +83,34 @@
     ParserInputSource parserInputSource =
         ParserInputSource.create(content, PathFragment.create(path.toString()));
 
-    List<RuleInfo> ruleInfoList = new SkydocMain().eval(parserInputSource);
+    ImmutableMap.Builder<String, RuleInfo> ruleInfoMap = ImmutableMap.builder();
+    ImmutableList.Builder<RuleInfo> unexportedRuleInfos = ImmutableList.builder();
+
+    new SkydocMain().eval(parserInputSource, ruleInfoMap, unexportedRuleInfos);
 
     try (PrintWriter printWriter = new PrintWriter(outputPath, "UTF-8")) {
-      printRuleInfos(printWriter, ruleInfoList);
+      printRuleInfos(printWriter, ruleInfoMap.build(), unexportedRuleInfos.build());
     }
   }
 
   // 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());
+      PrintWriter printWriter,
+      Map<String, RuleInfo> ruleInfos,
+      List<RuleInfo> unexportedRuleInfos) throws IOException {
+    for (Entry<String, RuleInfo> ruleInfoEntry : ruleInfos.entrySet()) {
+      printRuleInfo(printWriter, ruleInfoEntry.getKey(), ruleInfoEntry.getValue());
     }
+    for (RuleInfo unexportedRuleInfo : unexportedRuleInfos) {
+      printRuleInfo(printWriter, "<unknown name>", unexportedRuleInfo);
+    }
+  }
+
+  private static void printRuleInfo(
+      PrintWriter printWriter, String exportedName, RuleInfo ruleInfo) {
+    printWriter.println(exportedName);
+    printWriter.println(ruleInfo.getDescription());
+    printWriter.println();
   }
 
   /**
@@ -124,11 +118,17 @@
    * 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
+   * @param ruleInfoMap a map builder to be populated with rule definition information for
+   *     named rules. Keys are exported names of rules, and values are their {@link RuleInfo}
+   *     rule descriptions. For example, 'my_rule = rule(...)' has key 'my_rule'
+   * @param unexportedRuleInfos a list builder to be populated with rule definition information
+   *     for rules which were not exported as top level symbols
    * @throws InterruptedException if evaluation is interrupted
    */
   // TODO(cparsons): Evaluate load statements recursively.
-  public List<RuleInfo> eval(ParserInputSource parserInputSource)
+  public void eval(ParserInputSource parserInputSource,
+      ImmutableMap.Builder<String, RuleInfo> ruleInfoMap,
+      ImmutableList.Builder<RuleInfo> unexportedRuleInfos)
       throws InterruptedException {
     List<RuleInfo> ruleInfoList = new ArrayList<>();
 
@@ -146,7 +146,19 @@
 
     env.mutability().freeze();
 
-    return ruleInfoList;
+    Map<BaseFunction, RuleInfo> ruleFunctions = ruleInfoList.stream()
+        .collect(Collectors.toMap(
+            RuleInfo::getIdentifierFunction,
+            Functions.identity()));
+
+    for (Entry<String, Object> envEntry : env.getGlobals().getBindings().entrySet()) {
+      if (ruleFunctions.containsKey(envEntry.getValue())) {
+        ruleInfoMap.put(envEntry.getKey(), ruleFunctions.get(envEntry.getValue()));
+        ruleFunctions.remove(envEntry.getValue());
+      }
+    }
+
+    unexportedRuleInfos.addAll(ruleFunctions.values());
   }
 
   /**