// Copyright 2014 Google Inc. 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.base.Joiner;
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.Attribute;
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.
*/
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;
/**
* 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.
*/
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 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 html documentation in the exact format it should be written into the Build
* Encyclopedia (expanding variables).
*/
String getHtmlDocumentation() {
String expandedDoc = htmlDocumentation;
// Substituting variables
for (Entry docVariable : docVariables.entrySet()) {
expandedDoc = expandedDoc.replace("${" + docVariable.getKey() + "}",
expandBuiltInVariables(docVariable.getKey(), docVariable.getValue()));
}
expandedDoc = expandedDoc.replace("${" + DocgenConsts.VAR_ATTRIBUTE_SIGNATURE + "}",
generateAttributeSignatures());
expandedDoc = expandedDoc.replace("${" + DocgenConsts.VAR_ATTRIBUTE_DEFINITION + "}",
generateAttributeDefinitions(true));
return String.format("%s
\n\n%s", ruleName,
getDeprecatedString(hasFlag(DocgenConsts.FLAG_DEPRECATED)), ruleName, 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 the html code of the attribute definitions without the header and name
* attribute of the rule.
*/
String generateAttributeDefinitions() {
return generateAttributeDefinitions(false);
}
private String generateAttributeDefinitions(boolean generateNameAndHeader) {
StringBuilder sb = new StringBuilder();
if (generateNameAndHeader){
String nameExtraHtmlDoc = docVariables.containsKey(DocgenConsts.VAR_NAME)
? docVariables.get(DocgenConsts.VAR_NAME) : "";
sb.append(String.format(Joiner.on('\n').join(new String[] {
"Arguments
",
"",
"name
: A unique name for this rule.",
"(Name; required)%s \n"}),
ruleName, ruleName, nameExtraHtmlDoc));
} else {
sb.append("\n");
}
for (RuleDocumentationAttribute attributeDoc : attributes) {
// Only generate attribute documentation here if the rule and the attribute is
// either both user defined or built in (of common type).
if (isCommonType() == attributeDoc.isCommonType()) {
String attrName = attributeDoc.getAttributeName();
Attribute attribute = isCommonType() ? null
: ruleClassProvider.getRuleClassMap().get(ruleName).getAttributeByName(attrName);
sb.append(String.format("%s
:\n%s \n",
ruleName.toLowerCase(), attrName, getDeprecatedString(
attributeDoc.hasFlag(DocgenConsts.FLAG_DEPRECATED)),
attrName, attributeDoc.getHtmlDocumentation(attribute, ruleName)));
}
}
sb.append("
\n");
RuleClass ruleClass = ruleClassProvider.getRuleClassMap().get(ruleName);
if (ruleClass != null && ruleClass.isPublicByDefault()) {
sb.append(
"The default visibility is public: visibility = [\"//visibility:public\"]
.");
}
return sb.toString();
}
private String getDeprecatedString(boolean deprecated) {
return deprecated ? " class=\"deprecated\"" : "";
}
private String generateAttributeSignatures() {
StringBuilder sb = new StringBuilder();
sb.append(String.format(
"\n%s(name,\n",
ruleName, ruleName));
int i = 0;
for (RuleDocumentationAttribute attributeDoc : attributes) {
String attrName = attributeDoc.getAttributeName();
// Generate the link for the attribute documentation
sb.append(String.format("%s",
attributeDoc.getGeneratedInRule(ruleName).toLowerCase(), attrName, attrName));
if (i < attributes.size() - 1) {
sb.append(",");
} else {
sb.append(")");
}
sb.append("\n");
i++;
}
sb.append("
\n");
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;
}
/**
* Return true if the rule doesn't belong to a specific rule family.
*/
private boolean isCommonType() {
return ruleFamily == null;
}
/**
* 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);
}
}