|  | // 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.annotations.VisibleForTesting; | 
|  | import com.google.common.base.Preconditions; | 
|  | import com.google.common.collect.ImmutableSet; | 
|  | import com.google.devtools.build.docgen.DocgenConsts.RuleType; | 
|  | import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider; | 
|  | import com.google.devtools.build.lib.packages.RuleClass; | 
|  |  | 
|  | import java.util.HashMap; | 
|  | import java.util.HashSet; | 
|  | import java.util.Map; | 
|  | import java.util.Map.Entry; | 
|  | import java.util.Set; | 
|  | import java.util.TreeSet; | 
|  |  | 
|  | /** | 
|  | * A class representing the documentation of a rule along with some meta-data. The sole ruleName | 
|  | * field is used as a key for comparison, equals and hashcode. | 
|  | * | 
|  | * <p> The class contains meta information about the rule: | 
|  | * <ul> | 
|  | * <li> Rule type: categorizes the rule based on it's general (language independent) purpose, | 
|  | * see {@link RuleType}. | 
|  | * <li> Rule family: categorizes the rule based on language. | 
|  | * </ul> | 
|  | * | 
|  | * <p> The class also contains physical information about the documentation, | 
|  | * such as declaring file name and the first line of the raw documentation. This can be useful for | 
|  | * proper error signaling during documentation processing. | 
|  | */ | 
|  | public class RuleDocumentation implements Comparable<RuleDocumentation> { | 
|  | private final String ruleName; | 
|  | private final RuleType ruleType; | 
|  | private final String ruleFamily; | 
|  | private final String htmlDocumentation; | 
|  | // Store these information for error messages | 
|  | private final int startLineCount; | 
|  | private final String fileName; | 
|  | private final ImmutableSet<String> flags; | 
|  |  | 
|  | private final Map<String, String> docVariables = new HashMap<>(); | 
|  | // Only one attribute per attributeName is allowed | 
|  | private final Set<RuleDocumentationAttribute> attributes = new TreeSet<>(); | 
|  | private final ConfiguredRuleClassProvider ruleClassProvider; | 
|  |  | 
|  | private RuleLinkExpander linkExpander; | 
|  |  | 
|  | /** | 
|  | * Name of the page documenting common build rule terms and concepts. | 
|  | */ | 
|  | static final String COMMON_DEFINITIONS_PAGE = "common-definitions.html"; | 
|  |  | 
|  | /** | 
|  | * 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 { | 
|  | 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; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the name of the rule. | 
|  | */ | 
|  | public String getRuleName() { | 
|  | return ruleName; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the type of the rule | 
|  | */ | 
|  | RuleType getRuleType() { | 
|  | return ruleType; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the family of the rule. The family is usually the corresponding programming language, | 
|  | * except for rules independent of language, such as genrule. E.g. the family of the java_library | 
|  | * rule is 'JAVA', the family of genrule is 'GENERAL'. | 
|  | */ | 
|  | String getRuleFamily() { | 
|  | return ruleFamily; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns a "normalized" version of the input string. Used to convert rule family names into | 
|  | * strings that are more friendly as file names. For example, "C / C++" is converted to | 
|  | * "c-cpp". | 
|  | */ | 
|  | @VisibleForTesting | 
|  | static String normalize(String s) { | 
|  | return s.toLowerCase() | 
|  | .replace("+", "p") | 
|  | .replaceAll("[()]", "") | 
|  | .replaceAll("[\\s/]", "-") | 
|  | .replaceAll("[-]+", "-"); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the number of first line of the rule documentation in its declaration file. | 
|  | */ | 
|  | int getStartLineCount() { | 
|  | return startLineCount; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns true if this rule documentation has the parameter flag. | 
|  | */ | 
|  | boolean hasFlag(String flag) { | 
|  | return flags.contains(flag); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns true if this rule applies to a specific programming language (e.g. java_library), | 
|  | * returns false if it is a generic action (e.g. genrule, filegroup). | 
|  | * | 
|  | * <p>A rule is considered to be specific to a programming language by default. Generic rules | 
|  | * have to be marked with the flag GENERIC_RULE in their #BLAZE_RULE definition. | 
|  | */ | 
|  | boolean isLanguageSpecific() { | 
|  | return !flags.contains(DocgenConsts.FLAG_GENERIC_RULE); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Adds a variable name - value pair to the documentation to be substituted. | 
|  | */ | 
|  | void addDocVariable(String varName, String value) { | 
|  | docVariables.put(varName, value); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Adds a rule documentation attribute to this rule documentation. | 
|  | */ | 
|  | void addAttribute(RuleDocumentationAttribute attribute) { | 
|  | attributes.add(attribute); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the rule's set of RuleDocumentationAttributes. | 
|  | */ | 
|  | public Set<RuleDocumentationAttribute> getAttributes() { | 
|  | return attributes; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * 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() 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; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns the documentation of the rule in a form which is printable on the command line. | 
|  | */ | 
|  | String getCommandLineDocumentation() { | 
|  | return "\n" + DocgenConsts.toCommandLineFormat(htmlDocumentation); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns a string containing any extra documentation for the name attribute for this | 
|  | * rule. | 
|  | */ | 
|  | public String getNameExtraHtmlDoc() throws BuildEncyclopediaDocException { | 
|  | String expandedDoc = docVariables.containsKey(DocgenConsts.VAR_NAME) | 
|  | ? docVariables.get(DocgenConsts.VAR_NAME) | 
|  | : ""; | 
|  | if (linkExpander != null) { | 
|  | try { | 
|  | expandedDoc = linkExpander.expand(expandedDoc); | 
|  | } catch (IllegalArgumentException e) { | 
|  | throw new BuildEncyclopediaDocException(fileName, startLineCount, e.getMessage()); | 
|  | } | 
|  | } | 
|  | return expandedDoc; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns whether this rule has public visibility by default. | 
|  | */ | 
|  | public boolean isPublicByDefault() { | 
|  | RuleClass ruleClass = ruleClassProvider.getRuleClassMap().get(ruleName); | 
|  | return ruleClass != null && ruleClass.isPublicByDefault(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns whether this rule is deprecated. | 
|  | */ | 
|  | public boolean isDeprecated() { | 
|  | return hasFlag(DocgenConsts.FLAG_DEPRECATED); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns a string containing the attribute signature for this rule with HTML links | 
|  | * to the attributes. | 
|  | */ | 
|  | public String getAttributeSignature() { | 
|  | StringBuilder sb = new StringBuilder(); | 
|  | sb.append(String.format("%s(<a href=\"#%s.name\">name</a>, ", ruleName, ruleName)); | 
|  | int i = 0; | 
|  | for (RuleDocumentationAttribute attributeDoc : attributes) { | 
|  | String attrName = attributeDoc.getAttributeName(); | 
|  | // Generate the link for the attribute documentation | 
|  | if (attributeDoc.isCommonType()) { | 
|  | sb.append(String.format("<a href=\"%s#%s.%s\">%s</a>", | 
|  | COMMON_DEFINITIONS_PAGE, | 
|  | attributeDoc.getGeneratedInRule(ruleName).toLowerCase(), | 
|  | attrName, | 
|  | attrName)); | 
|  | } else { | 
|  | sb.append(String.format("<a href=\"#%s.%s\">%s</a>", | 
|  | attributeDoc.getGeneratedInRule(ruleName).toLowerCase(), | 
|  | attrName, | 
|  | attrName)); | 
|  | } | 
|  | if (i < attributes.size() - 1) { | 
|  | sb.append(", "); | 
|  | } else { | 
|  | sb.append(")"); | 
|  | } | 
|  | i++; | 
|  | } | 
|  | return sb.toString(); | 
|  | } | 
|  |  | 
|  | private String expandBuiltInVariables(String key, String value) { | 
|  | // Some built in BLAZE variables need special handling, e.g. adding headers | 
|  | switch (key) { | 
|  | case DocgenConsts.VAR_IMPLICIT_OUTPUTS: | 
|  | return String.format("<h4 id=\"%s_implicit_outputs\">Implicit output targets</h4>\n%s", | 
|  | ruleName.toLowerCase(), value); | 
|  | default: | 
|  | return value; | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Returns a set of examples based on markups which can be used as BUILD file | 
|  | * contents for testing. | 
|  | */ | 
|  | Set<String> extractExamples() throws BuildEncyclopediaDocException { | 
|  | String[] lines = htmlDocumentation.split(DocgenConsts.LS); | 
|  | Set<String> examples = new HashSet<>(); | 
|  | StringBuilder sb = null; | 
|  | boolean inExampleCode = false; | 
|  | int lineCount = 0; | 
|  | for (String line : lines) { | 
|  | if (!inExampleCode) { | 
|  | if (DocgenConsts.BLAZE_RULE_EXAMPLE_START.matcher(line).matches()) { | 
|  | inExampleCode = true; | 
|  | sb = new StringBuilder(); | 
|  | } else if (DocgenConsts.BLAZE_RULE_EXAMPLE_END.matcher(line).matches()) { | 
|  | throw new BuildEncyclopediaDocException(fileName, startLineCount + lineCount, | 
|  | "No matching start example tag (#BLAZE_RULE.EXAMPLE) for end example tag."); | 
|  | } | 
|  | } else { | 
|  | if (DocgenConsts.BLAZE_RULE_EXAMPLE_END.matcher(line).matches()) { | 
|  | inExampleCode = false; | 
|  | examples.add(sb.toString()); | 
|  | sb = null; | 
|  | } else if (DocgenConsts.BLAZE_RULE_EXAMPLE_START.matcher(line).matches()) { | 
|  | throw new BuildEncyclopediaDocException(fileName, startLineCount + lineCount, | 
|  | "No start example tags (#BLAZE_RULE.EXAMPLE) in a row."); | 
|  | } else { | 
|  | sb.append(line + DocgenConsts.LS); | 
|  | } | 
|  | } | 
|  | lineCount++; | 
|  | } | 
|  | return examples; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Creates a BuildEncyclopediaDocException with the file containing this rule doc and | 
|  | * the number of the first line (where the rule doc is defined). Can be used to create | 
|  | * general BuildEncyclopediaDocExceptions about this rule. | 
|  | */ | 
|  | BuildEncyclopediaDocException createException(String msg) { | 
|  | return new BuildEncyclopediaDocException(fileName, startLineCount, msg); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public int hashCode() { | 
|  | return ruleName.hashCode(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean equals(Object obj) { | 
|  | if (this == obj) { | 
|  | return true; | 
|  | } | 
|  | if (!(obj instanceof RuleDocumentation)) { | 
|  | return false; | 
|  | } | 
|  | return ruleName.equals(((RuleDocumentation) obj).ruleName); | 
|  | } | 
|  |  | 
|  | private int getTypePriority() { | 
|  | switch (ruleType) { | 
|  | case BINARY: | 
|  | return 1; | 
|  | case LIBRARY: | 
|  | return 2; | 
|  | case TEST: | 
|  | return 3; | 
|  | case OTHER: | 
|  | return 4; | 
|  | } | 
|  | throw new IllegalArgumentException("Illegal value of ruleType: " + ruleType); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public int compareTo(RuleDocumentation o) { | 
|  | if (this.getTypePriority() < o.getTypePriority()) { | 
|  | return -1; | 
|  | } else if (this.getTypePriority() > o.getTypePriority()) { | 
|  | return 1; | 
|  | } else { | 
|  | return this.ruleName.compareTo(o.ruleName); | 
|  | } | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public String toString() { | 
|  | return String.format("%s (TYPE = %s, FAMILY = %s)", ruleName, ruleType, ruleFamily); | 
|  | } | 
|  | } |