| // 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; | |
| /** | |
| * 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=\"common-definitions.html#%s.%s\">%s</a>", | |
| 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); | |
| } | |
| } |