Add syntax for referencing docs in other rule families.

This CL implements a new `${link rule.attribute}` syntax which can be used to
reference the documentation of rules and attributes of other rule families. For
example, `${link cc_library.deps}` will generate a link to the documentation for
the `deps` attribute of the `cc_library` rule. Similarly, this syntax can also
be used to reference sections of static documentation, for example
`${link common-definitions.label-expansion}`.

--
MOS_MIGRATED_REVID=115492361
diff --git a/src/main/java/com/google/devtools/build/docgen/BuildDocCollector.java b/src/main/java/com/google/devtools/build/docgen/BuildDocCollector.java
index 72de708..7bb4f71 100644
--- a/src/main/java/com/google/devtools/build/docgen/BuildDocCollector.java
+++ b/src/main/java/com/google/devtools/build/docgen/BuildDocCollector.java
@@ -122,10 +122,25 @@
     }
 
     processAttributeDocs(ruleDocEntries.values(), attributeDocEntries);
+    RuleLinkExpander expander = buildRuleLinkExpander(ruleDocEntries.values());
+    for (RuleDocumentation rule : ruleDocEntries.values()) {
+      rule.setRuleLinkExpander(expander);
+    }
     return ruleDocEntries;
   }
 
   /**
+   * Generates an index mapping rule name to its normalized rule family name.
+   */
+  private RuleLinkExpander buildRuleLinkExpander(Iterable<RuleDocumentation> rules) {
+    Map<String, String> index = new HashMap<>();
+    for (RuleDocumentation rule : rules) {
+      index.put(rule.getRuleName(), RuleFamily.normalize(rule.getRuleFamily()));
+    }
+    return new RuleLinkExpander(index);
+  }
+
+  /**
    * Go through all attributes of all documented rules and search the best attribute documentation
    * if exists. The best documentation is the closest documentation in the ancestor graph. E.g. if
    * java_library.deps documented in $rule and $java_rule then the one in $java_rule is going to
diff --git a/src/main/java/com/google/devtools/build/docgen/DocgenConsts.java b/src/main/java/com/google/devtools/build/docgen/DocgenConsts.java
index c0e2639..d50a299 100644
--- a/src/main/java/com/google/devtools/build/docgen/DocgenConsts.java
+++ b/src/main/java/com/google/devtools/build/docgen/DocgenConsts.java
@@ -93,6 +93,16 @@
   }
 
   /**
+   * Reference to another rule or Build Encyclopedia section.
+   *
+   * <p>The format of a link reference is rule.attribute (e.g. cc_library.deps). In the case of
+   * static pages such as common definitions the format is page.heading
+   * (e.g. common-definitions.label-expansion).
+   */
+  public static final Pattern BLAZE_RULE_LINK = Pattern.compile(
+      "\\$\\{link (([a-z_-]+)(\\.([a-z_-]+))?)\\}");
+
+  /**
    * i.e. <!-- #BLAZE_RULE(NAME = RULE_NAME, TYPE = RULE_TYPE, FAMILY = RULE_FAMILY) -->
    * i.e. <!-- #BLAZE_RULE(...)[DEPRECATED] -->
    */
diff --git a/src/main/java/com/google/devtools/build/docgen/RuleDocumentation.java b/src/main/java/com/google/devtools/build/docgen/RuleDocumentation.java
index 6da7f41..2fec7c4 100644
--- a/src/main/java/com/google/devtools/build/docgen/RuleDocumentation.java
+++ b/src/main/java/com/google/devtools/build/docgen/RuleDocumentation.java
@@ -57,28 +57,29 @@
   private final Set<RuleDocumentationAttribute> attributes = new TreeSet<>();
   private final ConfiguredRuleClassProvider ruleClassProvider;
 
+  private RuleLinkExpander linkExpander;
+
   /**
    * Creates a RuleDocumentation from the rule's name, type, family and raw html documentation
    * (meaning without expanding the variables in the doc).
    */
   RuleDocumentation(String ruleName, String ruleType, String ruleFamily,
       String htmlDocumentation, int startLineCount, String fileName, ImmutableSet<String> flags,
-      ConfiguredRuleClassProvider ruleClassProvider)
-          throws BuildEncyclopediaDocException {
+      ConfiguredRuleClassProvider ruleClassProvider) throws BuildEncyclopediaDocException {
     Preconditions.checkNotNull(ruleName);
-      this.ruleName = ruleName;
-      try {
-        this.ruleType = RuleType.valueOf(ruleType);
-      } catch (IllegalArgumentException e) {
-        throw new BuildEncyclopediaDocException(
-            fileName, startLineCount, "Invalid rule type " + ruleType);
-      }
-      this.ruleFamily = ruleFamily;
-      this.htmlDocumentation = htmlDocumentation;
-      this.startLineCount = startLineCount;
-      this.fileName = fileName;
-      this.flags = flags;
-      this.ruleClassProvider = ruleClassProvider;
+    this.ruleName = ruleName;
+    try {
+      this.ruleType = RuleType.valueOf(ruleType);
+    } catch (IllegalArgumentException e) {
+      throw new BuildEncyclopediaDocException(
+          fileName, startLineCount, "Invalid rule type " + ruleType);
+    }
+    this.ruleFamily = ruleFamily;
+    this.htmlDocumentation = htmlDocumentation;
+    this.startLineCount = startLineCount;
+    this.fileName = fileName;
+    this.flags = flags;
+    this.ruleClassProvider = ruleClassProvider;
   }
 
   /**
@@ -165,16 +166,35 @@
   }
 
   /**
+   * Sets the {@link RuleLinkExpander} to be used to expand links in the HTML documentation for
+   * both this RuleDocumentation and all {@link RuleDocumentationAttribute}s associated with this
+   * rule.
+   */
+  public void setRuleLinkExpander(RuleLinkExpander linkExpander) {
+    this.linkExpander = linkExpander;
+    for (RuleDocumentationAttribute attribute : attributes) {
+      attribute.setRuleLinkExpander(linkExpander);
+    }
+  }
+
+  /**
    * Returns the html documentation in the exact format it should be written into the Build
    * Encyclopedia (expanding variables).
    */
-  public String getHtmlDocumentation() {
+  public String getHtmlDocumentation() throws BuildEncyclopediaDocException {
     String expandedDoc = htmlDocumentation;
     // Substituting variables
     for (Entry<String, String> docVariable : docVariables.entrySet()) {
       expandedDoc = expandedDoc.replace("${" + docVariable.getKey() + "}",
           expandBuiltInVariables(docVariable.getKey(), docVariable.getValue()));
     }
+    if (linkExpander != null) {
+      try {
+        expandedDoc = linkExpander.expand(expandedDoc);
+      } catch (IllegalArgumentException e) {
+        throw new BuildEncyclopediaDocException(fileName, startLineCount, e.getMessage());
+      }
+    }
     return expandedDoc;
   }
 
diff --git a/src/main/java/com/google/devtools/build/docgen/RuleDocumentationAttribute.java b/src/main/java/com/google/devtools/build/docgen/RuleDocumentationAttribute.java
index 518a2c4..b1b60fb 100644
--- a/src/main/java/com/google/devtools/build/docgen/RuleDocumentationAttribute.java
+++ b/src/main/java/com/google/devtools/build/docgen/RuleDocumentationAttribute.java
@@ -60,7 +60,10 @@
   private final String attributeName;
   private final String htmlDocumentation;
   private final String commonType;
+  // Used to expand rule link references in the attribute documentation.
+  private RuleLinkExpander linkExpander;
   private int startLineCnt;
+  private String fileName;
   private Set<String> flags;
   private Attribute attribute;
 
@@ -71,7 +74,7 @@
   static RuleDocumentationAttribute create(
       String attributeName, String commonType, String htmlDocumentation) {
     RuleDocumentationAttribute docAttribute = new RuleDocumentationAttribute(
-        null, attributeName, htmlDocumentation, 0, ImmutableSet.<String>of(), commonType);
+        null, attributeName, htmlDocumentation, 0, "", ImmutableSet.<String>of(), commonType);
     return docAttribute;
   }
 
@@ -80,14 +83,15 @@
    * defined rule attributes.
    */
   static RuleDocumentationAttribute create(Class<? extends RuleDefinition> definitionClass,
-      String attributeName, String htmlDocumentation, int startLineCnt, Set<String> flags) {
+      String attributeName, String htmlDocumentation, int startLineCnt, String fileName,
+      Set<String> flags) {
     return new RuleDocumentationAttribute(definitionClass, attributeName, htmlDocumentation,
-        startLineCnt, flags, null);
+        startLineCnt, fileName, flags, null);
   }
 
   private RuleDocumentationAttribute(Class<? extends RuleDefinition> definitionClass,
-      String attributeName, String htmlDocumentation, int startLineCnt, Set<String> flags,
-      String commonType) {
+      String attributeName, String htmlDocumentation, int startLineCnt, String fileName,
+      Set<String> flags, String commonType) {
     Preconditions.checkNotNull(attributeName, "AttributeName must not be null.");
     this.definitionClass = definitionClass;
     this.attributeName = attributeName;
@@ -119,10 +123,25 @@
   }
 
   /**
-   * Returns the raw html documentation of the rule attribute.
+   * Sets the {@link RuleLinkExpander} to be used to expand links in the HTML documentation.
    */
-  public String getHtmlDocumentation() {
-    return htmlDocumentation;
+  public void setRuleLinkExpander(RuleLinkExpander linkExpander) {
+    this.linkExpander = linkExpander;
+  }
+
+  /**
+   * Returns the html documentation of the rule attribute.
+   */
+  public String getHtmlDocumentation() throws BuildEncyclopediaDocException {
+    String expandedHtmlDoc = htmlDocumentation;
+    if (linkExpander != null) {
+      try {
+        expandedHtmlDoc = linkExpander.expand(expandedHtmlDoc);
+      } catch (IllegalArgumentException e) {
+        throw new BuildEncyclopediaDocException(fileName, startLineCnt, e.getMessage());
+      }
+    }
+    return expandedHtmlDoc;
   }
 
   private String getDefaultValue() {
diff --git a/src/main/java/com/google/devtools/build/docgen/RuleLinkExpander.java b/src/main/java/com/google/devtools/build/docgen/RuleLinkExpander.java
new file mode 100644
index 0000000..8ac8cbc
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/docgen/RuleLinkExpander.java
@@ -0,0 +1,106 @@
+// Copyright 2014 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.docgen;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+
+/**
+ * Helper class used for expanding link references in rule and attribute documentation.
+ *
+ * <p>See {@link com.google.devtools.build.docgen.DocgenConsts.BLAZE_RULE_LINK} for the regex used
+ * to match link references.
+ */
+class RuleLinkExpander {
+  private static final String EXAMPLES_SUFFIX = "_examples";
+  private static final String ARGS_SUFFIX = "_args";
+
+  private static final Set<String> STATIC_PAGES = ImmutableSet.<String>of(
+      "common-definitions",
+      "make-variables",
+      "predefined-python-variables");
+
+  private final Map<String, String> ruleIndex;
+
+  RuleLinkExpander(Map<String, String> ruleIndex) {
+    this.ruleIndex = ruleIndex;
+  }
+
+  private void appendRuleLink(Matcher matcher, StringBuffer sb, String ruleName, String ref) {
+    String ruleFamily = ruleIndex.get(ruleName);
+    String link = ruleFamily + ".html#" + ref;
+    matcher.appendReplacement(sb, Matcher.quoteReplacement(link));
+  }
+
+  /**
+   * Expands all rule references in the input HTML documentation.
+   *
+   * @param htmlDoc The input HTML documentation with ${link foo.bar} references.
+   * @return The HTML documentation with all link references expanded.
+   */
+  public String expand(String htmlDoc) throws IllegalArgumentException {
+    Matcher matcher = DocgenConsts.BLAZE_RULE_LINK.matcher(htmlDoc);
+    StringBuffer sb = new StringBuffer(htmlDoc.length());
+    while (matcher.find()) {
+      // The first capture group matches the entire reference, e.g. "cc_binary.deps".
+      String ref = matcher.group(1);
+      // The second capture group only matches the rule name, e.g. "cc_binary" in "cc_binary.deps".
+      String name = matcher.group(2);
+
+      // The name in the reference is the name of a rule. Get the rule family for the rule and
+      // replace the reference with a link with the form of rule-family.html#rule.attribute. For
+      // example, ${link cc_library.deps} expands to c-cpp.html#cc_library.deps.
+      if (ruleIndex.containsKey(name)) {
+        appendRuleLink(matcher, sb, name, ref);
+        continue;
+      }
+
+      // The name is referencing the examples or arguments of a rule (e.g. "cc_library_args" or
+      // "cc_library_examples"). Strip the suffix and then try matching the name to a rule family.
+      if (name.endsWith(EXAMPLES_SUFFIX) || name.endsWith(ARGS_SUFFIX)) {
+        String ruleName = name.substring(
+            0, name.indexOf(name.endsWith(EXAMPLES_SUFFIX) ? EXAMPLES_SUFFIX : ARGS_SUFFIX));
+        if (ruleIndex.containsKey(ruleName)) {
+          appendRuleLink(matcher, sb, ruleName, ref);
+          continue;
+        }
+      }
+
+      // The name is not the name of a rule but is the name of a static page, such as
+      // common-definitions. Generate a link to that page, and append the page heading if
+      // specified. For example, ${link common-definitions.label-expansion} expands to
+      // common-definitions.html#label-expansion.
+      if (STATIC_PAGES.contains(name)) {
+        String link = name + ".html";
+        // The fourth capture group matches the attribute name, or page heading, e.g.
+        // "label-expansion" in "common-definitions.label-expansion".
+        String pageHeading = matcher.group(4);
+        if (pageHeading != null) {
+          link = link + "#" + pageHeading;
+        }
+        matcher.appendReplacement(sb, Matcher.quoteReplacement(link));
+        continue;
+      }
+
+      // If the reference does not match any rule or static page, throw an exception.
+      throw new IllegalArgumentException(
+          "link tag does not match any rule or BE page: " + matcher.group());
+    }
+    matcher.appendTail(sb);
+    return sb.toString();
+  }
+}
diff --git a/src/main/java/com/google/devtools/build/docgen/SourceFileReader.java b/src/main/java/com/google/devtools/build/docgen/SourceFileReader.java
index 651b46e..78f0b6d 100644
--- a/src/main/java/com/google/devtools/build/docgen/SourceFileReader.java
+++ b/src/main/java/com/google/devtools/build/docgen/SourceFileReader.java
@@ -214,7 +214,7 @@
         // End of a attribute, create RuleDocumentationAttribute object
         docAttributes.put(attributeName, RuleDocumentationAttribute.create(
             ruleClassProvider.getRuleClassDefinition(ruleName),
-            attributeName, sb.toString(), startLineCnt, flags));
+            attributeName, sb.toString(), startLineCnt, javaSourceFilePath, flags));
         sb = new StringBuilder();
         inBlazeAttributeDocs = false;
       }