// 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.
*
*
The class contains meta information about the rule:
*
* - Rule type: categorizes the rule based on it's general (language independent) purpose,
* see {@link RuleType}.
*
- Rule family: categorizes the rule based on language.
*
*
* 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 {
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 flags;
private final Map docVariables = new HashMap<>();
// Only one attribute per attributeName is allowed
private final Set 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 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).
*
* 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 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 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(name, ", 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("%s",
attributeDoc.getGeneratedInRule(ruleName).toLowerCase(),
attrName,
attrName));
} else {
sb.append(String.format("%s",
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("Implicit output targets
\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 extractExamples() throws BuildEncyclopediaDocException {
String[] lines = htmlDocumentation.split(DocgenConsts.LS);
Set 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);
}
}