| // 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.base.Preconditions; | |
| import com.google.common.collect.ImmutableMap; | |
| import com.google.common.collect.ImmutableSet; | |
| import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider; | |
| import com.google.devtools.build.lib.analysis.RuleDefinition; | |
| import com.google.devtools.build.lib.packages.Attribute; | |
| import com.google.devtools.build.lib.packages.Attribute.ComputedDefault; | |
| import com.google.devtools.build.lib.packages.Attribute.StarlarkComputedDefaultTemplate; | |
| import com.google.devtools.build.lib.packages.BuildType; | |
| import com.google.devtools.build.lib.packages.TriState; | |
| import com.google.devtools.build.lib.packages.Type; | |
| import com.google.devtools.build.lib.packages.Types; | |
| import com.google.devtools.build.lib.starlarkdocextract.LabelRenderer; | |
| import com.google.devtools.build.lib.starlarkdocextract.StardocOutputProtos.AttributeInfo; | |
| import java.util.HashMap; | |
| import java.util.LinkedList; | |
| import java.util.Map; | |
| import java.util.Set; | |
| import javax.annotation.Nullable; | |
| /** | |
| * A class storing a rule attribute documentation along with some meta information. For native | |
| * attributes, the class provides functionality to compute the ancestry level of this attribute's | |
| * generator rule definition class compared to other rule definition classes. | |
| * | |
| * <p>Warning, two RuleDocumentationAttribute objects are equal based on only the attributeName. | |
| */ | |
| public class RuleDocumentationAttribute | |
| implements Comparable<RuleDocumentationAttribute>, Cloneable { | |
| private static final ImmutableMap<Type<?>, String> TYPE_DESC = | |
| ImmutableMap.<Type<?>, String>builder() | |
| .put(Type.BOOLEAN, "Boolean") | |
| .put(Type.INTEGER, "Integer") | |
| .put(Types.INTEGER_LIST, "List of integers") | |
| .put(Type.STRING, "String") | |
| .put(Types.STRING_DICT, "Dictionary: String -> String") | |
| .put(Types.STRING_LIST, "List of strings") | |
| .put(BuildType.TRISTATE, "Integer") | |
| .put(BuildType.LABEL, "<a href=\"${link build-ref#labels}\">Label</a>") | |
| .put( | |
| BuildType.LABEL_KEYED_STRING_DICT, | |
| "Dictionary: <a href=\"${link build-ref#labels}\">label</a> -> String") | |
| .put(BuildType.LABEL_LIST, "List of <a href=\"${link build-ref#labels}\">labels</a>") | |
| .put( | |
| BuildType.GENQUERY_SCOPE_TYPE_LIST, | |
| "List of <a href=\"${link build-ref#labels}\">labels</a>") | |
| .put( | |
| BuildType.LABEL_DICT_UNARY, | |
| "Dictionary mapping strings to <a href=\"${link build-ref#labels}\">labels</a>") | |
| .put(BuildType.LICENSE, "Licence type") | |
| .put(BuildType.NODEP_LABEL, "<a href=\"${link build-ref#name}\">Name</a>") | |
| .put(BuildType.NODEP_LABEL_LIST, "List of <a href=\"${link build-ref#name}\">names</a>") | |
| .put(BuildType.OUTPUT, "<a href=\"${link build-ref#filename}\">Filename</a>") | |
| .put( | |
| BuildType.OUTPUT_LIST, "List of <a href=\"${link build-ref#filename}\">filenames</a>") | |
| .buildOrThrow(); | |
| @Nullable private final Class<? extends RuleDefinition> definitionClass; | |
| private final String attributeName; | |
| private final String htmlDocumentation; | |
| @Nullable private final String commonType; | |
| // Used to expand rule link references in the attribute documentation. | |
| private RuleLinkExpander linkExpander; | |
| private final String location; // for error messages | |
| private Set<String> flags; | |
| // The following are not set by create() or createCommon() | |
| @Nullable private final Type<?> type; | |
| @Nullable private final String defaultValue; | |
| private final boolean mandatory; | |
| private final boolean nonconfigurable; | |
| /** | |
| * Creates a RuleDocumentationAttribute from comments in Java sources. Additional metadata may be | |
| * filled in later via {@link copyAndUpdateFrom}. | |
| */ | |
| static RuleDocumentationAttribute create( | |
| @Nullable Class<? extends RuleDefinition> definitionClass, | |
| String attributeName, | |
| String htmlDocumentation, | |
| String file, | |
| int lineNumber, | |
| Set<String> flags) { | |
| return new RuleDocumentationAttribute( | |
| definitionClass, | |
| attributeName, | |
| htmlDocumentation, | |
| BuildEncyclopediaDocException.formatLocation(file, lineNumber), | |
| flags, | |
| /* commonType= */ null, | |
| /* type= */ null, | |
| /* defaultValue= */ null, | |
| /* mandatory= */ false, | |
| /* nonconfigurable= */ false); | |
| } | |
| /** | |
| * Creates common RuleDocumentationAttribute such as deps or data. These attribute docs have no | |
| * definitionClass or htmlDocumentation (it's in the BE header). | |
| */ | |
| static RuleDocumentationAttribute createCommon( | |
| String attributeName, String commonType, String htmlDocumentation) { | |
| return new RuleDocumentationAttribute( | |
| null, | |
| attributeName, | |
| htmlDocumentation, | |
| "", | |
| ImmutableSet.of(), | |
| commonType, | |
| /* type= */ null, | |
| /* defaultValue= */ null, | |
| /* mandatory= */ false, | |
| /* nonconfigurable= */ false); | |
| } | |
| /** Creates a RuleDocumentationAttribute from a stardoc_output.AttributeInfo proto. */ | |
| static RuleDocumentationAttribute createFromAttributeInfo( | |
| AttributeInfo attributeInfo, String location, Set<String> flags) | |
| throws BuildEncyclopediaDocException { | |
| return new RuleDocumentationAttribute( | |
| null, | |
| attributeInfo.getName(), | |
| attributeInfo.getDocString(), | |
| location, | |
| flags, | |
| /* commonType= */ null, | |
| getAttributeInfoType(attributeInfo, location), | |
| attributeInfo.getDefaultValue(), | |
| attributeInfo.getMandatory(), | |
| attributeInfo.getNonconfigurable()); | |
| } | |
| /** | |
| * Copies this RuleDocumentationAttribute and sets additional metadata (type, default value, and | |
| * whether the attribute is mandatory or nonconfigurable) from a native attribute object. | |
| */ | |
| RuleDocumentationAttribute copyAndUpdateFrom(Attribute attribute) { | |
| return new RuleDocumentationAttribute( | |
| this.definitionClass, | |
| this.attributeName, | |
| this.htmlDocumentation, | |
| this.location, | |
| this.flags, | |
| this.commonType, | |
| attribute.getType(), | |
| reprDefaultValue(attribute), | |
| attribute.isMandatory(), | |
| !attribute.isConfigurable()); | |
| } | |
| private static Type<?> getAttributeInfoType(AttributeInfo attributeInfo, String location) | |
| throws BuildEncyclopediaDocException { | |
| return switch (attributeInfo.getType()) { | |
| case INT -> Type.INTEGER; | |
| case LABEL -> BuildType.LABEL; | |
| case NAME, STRING -> Type.STRING; | |
| case STRING_LIST -> Types.STRING_LIST; | |
| case INT_LIST -> Types.INTEGER_LIST; | |
| case LABEL_LIST -> BuildType.LABEL_LIST; | |
| case BOOLEAN -> Type.BOOLEAN; | |
| case LABEL_STRING_DICT -> BuildType.LABEL_KEYED_STRING_DICT; | |
| case STRING_DICT -> Types.STRING_DICT; | |
| case STRING_LIST_DICT -> Types.STRING_LIST_DICT; | |
| case LABEL_DICT_UNARY -> BuildType.LABEL_DICT_UNARY; | |
| case LABEL_LIST_DICT -> BuildType.LABEL_LIST_DICT; | |
| case OUTPUT -> BuildType.OUTPUT; | |
| case OUTPUT_LIST -> BuildType.OUTPUT_LIST; | |
| default -> | |
| throw new BuildEncyclopediaDocException( | |
| location, | |
| String.format( | |
| "attribute %s: unknown type %s", | |
| attributeInfo.getName(), attributeInfo.getType())); | |
| }; | |
| } | |
| private RuleDocumentationAttribute( | |
| @Nullable Class<? extends RuleDefinition> definitionClass, | |
| String attributeName, | |
| String htmlDocumentation, | |
| String location, | |
| Set<String> flags, | |
| @Nullable String commonType, | |
| @Nullable Type<?> type, | |
| @Nullable String defaultValue, | |
| boolean mandatory, | |
| boolean nonconfigurable) { | |
| Preconditions.checkNotNull(attributeName, "AttributeName must not be null."); | |
| this.definitionClass = definitionClass; | |
| this.attributeName = attributeName; | |
| this.htmlDocumentation = htmlDocumentation; | |
| this.location = location; | |
| this.flags = flags; | |
| this.commonType = commonType; | |
| this.type = type; | |
| this.defaultValue = defaultValue; | |
| this.mandatory = mandatory; | |
| this.nonconfigurable = nonconfigurable; | |
| } | |
| @Nullable | |
| private static String reprDefaultValue(Attribute attribute) { | |
| Object value = attribute.getDefaultValueUnchecked(); | |
| if (value instanceof ComputedDefault || value instanceof StarlarkComputedDefaultTemplate) { | |
| // We cannot print anything useful here other than "optional". Let's assume the doc string for | |
| // the attribute explains the details. | |
| return null; | |
| } else if (value instanceof TriState triState) { | |
| return switch (triState) { | |
| case AUTO -> "-1"; | |
| case NO -> "0"; | |
| case YES -> "1"; | |
| }; | |
| } | |
| return LabelRenderer.DEFAULT.reprWithoutLabelConstructor(Attribute.valueToStarlark(value)); | |
| } | |
| /** | |
| * Returns the name of the rule attribute. | |
| */ | |
| public String getAttributeName() { | |
| return attributeName; | |
| } | |
| /** | |
| * Returns the file name or label, optionally with a line number, where the rule attribute is | |
| * defined. | |
| */ | |
| public String getLocation() { | |
| return location; | |
| } | |
| /** Returns the attribute's default value, or null if none set. */ | |
| @Nullable | |
| public String getDefaultValue() { | |
| // Strings are stored as "foo". Remove the surrounding quotes. | |
| return defaultValue == null ? null : defaultValue.substring(1, defaultValue.length() - 1); | |
| } | |
| /** | |
| * Returns whether this attribute is marked as deprecated. | |
| */ | |
| public boolean isDeprecated() { | |
| return hasFlag(DocgenConsts.FLAG_DEPRECATED); | |
| } | |
| /** | |
| * Sets the {@link RuleLinkExpander} to be used to expand links in the HTML documentation. | |
| */ | |
| public void setRuleLinkExpander(RuleLinkExpander linkExpander) { | |
| this.linkExpander = linkExpander; | |
| } | |
| /** | |
| * Returns the html documentation of the rule attribute. | |
| */ | |
| public String getHtmlDocumentation() throws BuildEncyclopediaDocException { | |
| return tryExpand(htmlDocumentation); | |
| } | |
| public String tryExpand(String html) throws BuildEncyclopediaDocException { | |
| if (linkExpander == null) { | |
| return html; | |
| } | |
| try { | |
| return linkExpander.expand(html); | |
| } catch (IllegalArgumentException e) { | |
| throw new BuildEncyclopediaDocException(location, e.getMessage()); | |
| } | |
| } | |
| /** Returns whether the param is required or optional. */ | |
| public boolean isMandatory() { | |
| return mandatory; | |
| } | |
| /** Returns a string containing the synopsis for this attribute. */ | |
| public String getSynopsis() throws BuildEncyclopediaDocException { | |
| if (type == null) { | |
| return ""; | |
| } | |
| String rawType = TYPE_DESC.get(type); | |
| StringBuilder sb = | |
| new StringBuilder() | |
| .append(rawType == null ? null : tryExpand(rawType)) | |
| .append( | |
| nonconfigurable | |
| ? String.format( | |
| "; <a href=\"%s#configurable-attributes\">nonconfigurable</a>", | |
| RuleDocumentation.COMMON_DEFINITIONS_PAGE) | |
| : ""); | |
| if (isMandatory()) { | |
| sb.append("; required"); | |
| } else if (defaultValue != null && !defaultValue.isEmpty()) { | |
| sb.append("; default is <code>").append(defaultValue).append("</code>"); | |
| } else { | |
| // Computed default or other non-representable value | |
| sb.append("; optional"); | |
| } | |
| return sb.toString(); | |
| } | |
| /** | |
| * Returns true if the attribute doc is of a common attribute type. | |
| */ | |
| public boolean isCommonType() { | |
| return commonType != null; | |
| } | |
| /** | |
| * Returns the common attribute type if this attribute doc is of a common type | |
| * otherwise actualRule. | |
| */ | |
| String getGeneratedInRule(String actualRule) { | |
| return isCommonType() ? commonType : actualRule; | |
| } | |
| /** | |
| * Returns true if this attribute documentation has the parameter flag. | |
| */ | |
| boolean hasFlag(String flag) { | |
| return flags.contains(flag); | |
| } | |
| /** | |
| * Returns the length of a shortest path from usingClass to the definitionClass of this | |
| * RuleDocumentationAttribute in the rule definition ancestry graph. Returns -1 | |
| * if definitionClass is not the ancestor (transitively) of usingClass. | |
| */ | |
| int getDefinitionClassAncestryLevel(Class<? extends RuleDefinition> usingClass, | |
| ConfiguredRuleClassProvider ruleClassProvider) { | |
| if (usingClass.equals(definitionClass)) { | |
| return 0; | |
| } | |
| // Storing nodes (rule class definitions) with the length of the shortest path from usingClass | |
| Map<Class<? extends RuleDefinition>, Integer> visited = new HashMap<>(); | |
| LinkedList<Class<? extends RuleDefinition>> toVisit = new LinkedList<>(); | |
| visited.put(usingClass, 0); | |
| toVisit.add(usingClass); | |
| // Searching the shortest path from usingClass to this.definitionClass using BFS | |
| do { | |
| Class<? extends RuleDefinition> ancestor = toVisit.removeFirst(); | |
| visitAncestor(ancestor, visited, toVisit, ruleClassProvider); | |
| if (ancestor.equals(definitionClass)) { | |
| return visited.get(ancestor); | |
| } | |
| } while (!toVisit.isEmpty()); | |
| return -1; | |
| } | |
| private void visitAncestor( | |
| Class<? extends RuleDefinition> usingClass, | |
| Map<Class<? extends RuleDefinition>, Integer> visited, | |
| LinkedList<Class<? extends RuleDefinition>> toVisit, | |
| ConfiguredRuleClassProvider ruleClassProvider) { | |
| RuleDefinition instance = getRuleDefinition(usingClass, ruleClassProvider); | |
| for (Class<? extends RuleDefinition> ancestor : instance.getMetadata().ancestors()) { | |
| if (!visited.containsKey(ancestor)) { | |
| toVisit.addLast(ancestor); | |
| visited.put(ancestor, visited.get(usingClass) + 1); | |
| } | |
| } | |
| } | |
| private RuleDefinition getRuleDefinition(Class<? extends RuleDefinition> usingClass, | |
| ConfiguredRuleClassProvider ruleClassProvider) { | |
| if (ruleClassProvider == null) { | |
| try { | |
| return usingClass.getConstructor().newInstance(); | |
| } catch (ReflectiveOperationException | IllegalArgumentException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| } | |
| return ruleClassProvider.getRuleClassDefinition(usingClass.getName()); | |
| } | |
| private int getAttributeOrderingPriority(RuleDocumentationAttribute attribute) { | |
| if (DocgenConsts.ATTRIBUTE_ORDERING.containsKey(attribute.attributeName)) { | |
| return DocgenConsts.ATTRIBUTE_ORDERING.get(attribute.attributeName); | |
| } else { | |
| return 0; | |
| } | |
| } | |
| @Override | |
| public int compareTo(RuleDocumentationAttribute o) { | |
| int thisPriority = getAttributeOrderingPriority(this); | |
| int otherPriority = getAttributeOrderingPriority(o); | |
| if (thisPriority > otherPriority) { | |
| return 1; | |
| } else if (thisPriority < otherPriority) { | |
| return -1; | |
| } else { | |
| return this.attributeName.compareTo(o.attributeName); | |
| } | |
| } | |
| @Override | |
| public boolean equals(Object obj) { | |
| if (this == obj) { | |
| return true; | |
| } | |
| if (!(obj instanceof RuleDocumentationAttribute ruleDocumentationAttribute)) { | |
| return false; | |
| } | |
| return attributeName.equals(ruleDocumentationAttribute.attributeName); | |
| } | |
| @Override | |
| public int hashCode() { | |
| return attributeName.hashCode(); | |
| } | |
| } |