| // 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. | |
| * | |
| * <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. | |
| */ | |
| 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; | |
| /** | |
| * 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. | |
| */ | |
| 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<String, String> 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("<h3 id=\"%s\"%s>%s</h3>\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[] { | |
| "<h4 id=\"%s_args\">Arguments</h4>", | |
| "<ul>", | |
| "<li id=\"%s.name\"><code>name</code>: A unique name for this rule.", | |
| "<i>(<a href=\"build-ref.html#name\">Name</a>; required)</i>%s</li>\n"}), | |
| ruleName, ruleName, nameExtraHtmlDoc)); | |
| } else { | |
| sb.append("<ul>\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("<li id=\"%s.%s\"%s><code>%s</code>:\n%s</li>\n", | |
| ruleName.toLowerCase(), attrName, getDeprecatedString( | |
| attributeDoc.hasFlag(DocgenConsts.FLAG_DEPRECATED)), | |
| attrName, attributeDoc.getHtmlDocumentation(attribute, ruleName))); | |
| } | |
| } | |
| sb.append("</ul>\n"); | |
| RuleClass ruleClass = ruleClassProvider.getRuleClassMap().get(ruleName); | |
| if (ruleClass != null && ruleClass.isPublicByDefault()) { | |
| sb.append( | |
| "The default visibility is public: <code>visibility = [\"//visibility:public\"]</code>."); | |
| } | |
| return sb.toString(); | |
| } | |
| private String getDeprecatedString(boolean deprecated) { | |
| return deprecated ? " class=\"deprecated\"" : ""; | |
| } | |
| private String generateAttributeSignatures() { | |
| StringBuilder sb = new StringBuilder(); | |
| sb.append(String.format( | |
| "<p class=\"rule-signature\">\n%s(<a href=\"#%s.name\">name</a>,\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("<a href=\"#%s.%s\">%s</a>", | |
| attributeDoc.getGeneratedInRule(ruleName).toLowerCase(), attrName, attrName)); | |
| if (i < attributes.size() - 1) { | |
| sb.append(","); | |
| } else { | |
| sb.append(")"); | |
| } | |
| sb.append("\n"); | |
| i++; | |
| } | |
| sb.append("</p>\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("<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; | |
| } | |
| /** | |
| * 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); | |
| } | |
| } |