|  | // 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 static java.nio.charset.StandardCharsets.UTF_8; | 
|  |  | 
|  | import com.google.auto.value.AutoValue; | 
|  | import com.google.common.annotations.VisibleForTesting; | 
|  | import com.google.common.base.Ascii; | 
|  | import com.google.common.base.Splitter; | 
|  | import com.google.common.base.Strings; | 
|  | import com.google.common.collect.ImmutableSet; | 
|  | import com.google.common.collect.LinkedListMultimap; | 
|  | import com.google.common.collect.ListMultimap; | 
|  | import com.google.common.flogger.GoogleLogger; | 
|  | import com.google.devtools.build.docgen.DocgenConsts.RuleType; | 
|  | 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.RuleClass; | 
|  | import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.AttributeInfo; | 
|  | import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.ModuleInfo; | 
|  | import com.google.devtools.build.skydoc.rendering.proto.StardocOutputProtos.RuleInfo; | 
|  | import com.google.protobuf.ExtensionRegistry; | 
|  | import java.io.BufferedReader; | 
|  | import java.io.File; | 
|  | import java.io.FileInputStream; | 
|  | import java.io.IOException; | 
|  | import java.nio.file.Files; | 
|  | import java.util.HashMap; | 
|  | import java.util.HashSet; | 
|  | import java.util.List; | 
|  | import java.util.Map; | 
|  | import java.util.Set; | 
|  | import java.util.TreeMap; | 
|  | import java.util.regex.Matcher; | 
|  |  | 
|  | /** | 
|  | * Class that parses the documentation fragments of rule-classes and | 
|  | * generates the html format documentation. | 
|  | */ | 
|  | @VisibleForTesting | 
|  | public class BuildDocCollector { | 
|  | private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); | 
|  |  | 
|  | private static final Splitter SHARP_SPLITTER = Splitter.on('#').limit(2).trimResults(); | 
|  |  | 
|  | private final RuleLinkExpander linkExpander; | 
|  | private final SourceUrlMapper urlMapper; | 
|  | private final ConfiguredRuleClassProvider ruleClassProvider; | 
|  |  | 
|  | public BuildDocCollector( | 
|  | RuleLinkExpander linkExpander, | 
|  | SourceUrlMapper urlMapper, | 
|  | ConfiguredRuleClassProvider ruleClassProvider) { | 
|  | this.linkExpander = linkExpander; | 
|  | this.urlMapper = urlMapper; | 
|  | this.ruleClassProvider = ruleClassProvider; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Parse the file containing rules blocked from documentation. The list is simply a list of rules | 
|  | * separated by new lines. Line comments can be added to the file by starting them with #. | 
|  | * | 
|  | * @param denyList The name of the file containing the denylist. | 
|  | * @return The set of denylisted rules. | 
|  | * @throws IOException | 
|  | */ | 
|  | @VisibleForTesting | 
|  | public static Set<String> readDenyList(String denyList) throws IOException { | 
|  | Set<String> result = new HashSet<String>(); | 
|  | if (denyList != null && !denyList.isEmpty()) { | 
|  | File file = new File(denyList); | 
|  | try (BufferedReader reader = Files.newBufferedReader(file.toPath(), UTF_8)) { | 
|  | for (String line = reader.readLine(); line != null; line = reader.readLine()) { | 
|  | String rule = SHARP_SPLITTER.split(line).iterator().next(); | 
|  | if (!rule.isEmpty()) { | 
|  | result.add(rule); | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  | return result; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Creates a map of rule names (keys) to rule documentation (values). | 
|  | * | 
|  | * <p>This method crawls the specified input directories for rule class definitions (as Java | 
|  | * source files) which contain the rules' and attributes' definitions as comments in a specific | 
|  | * format. The keys in the returned Map correspond to these rule classes. | 
|  | * | 
|  | * <p>In the Map's values, all references pointing to other rules, rule attributes, and general | 
|  | * documentation (e.g. common definitions, make variables, etc.) are expanded into hyperlinks. The | 
|  | * links generated follow either the multi-page or single-page Build Encyclopedia model depending | 
|  | * on the mode set for the {@link RuleLinkExpander} that was passed to the constructor. | 
|  | * | 
|  | * @param inputJavaDirs list of directories to scan for documentation in Java source code | 
|  | * @param inputStardocProtos list of file paths of stardoc_output.ModuleInfo binary proto files | 
|  | *     generated from Build Encyclopedia entry point .bzl files; documentation from these protos | 
|  | *     takes precedence over documentation from {@code inputJavaDirs} | 
|  | * @param denyList specify an optional denylist file that list some rules that should not be | 
|  | *     listed in the output. | 
|  | * @throws BuildEncyclopediaDocException | 
|  | * @throws IOException | 
|  | * @return Map of rule class to rule documentation. | 
|  | */ | 
|  | public Map<String, RuleDocumentation> collect( | 
|  | List<String> inputJavaDirs, List<String> inputStardocProtos, String denyList) | 
|  | throws BuildEncyclopediaDocException, IOException { | 
|  | // Read the denyList file | 
|  | Set<String> denylistedRules = readDenyList(denyList); | 
|  | // RuleDocumentations are generated in order (based on rule type then alphabetically). | 
|  | // The ordering is also used to determine in which rule doc the common attribute docs are | 
|  | // generated (they are generated at the first appearance). | 
|  | Map<String, RuleDocumentation> ruleDocEntries = new TreeMap<>(); | 
|  | // RuleDocumentationAttribute objects equal based on attributeName so they have to be | 
|  | // collected in a List instead of a Set. | 
|  | ListMultimap<String, RuleDocumentationAttribute> attributeDocEntries = | 
|  | LinkedListMultimap.create(); | 
|  |  | 
|  | // Map of rule name to the file (Java source file or Build Encyclopedia entry point .bzl file) | 
|  | // and symbol from which its documentation was obtained. | 
|  | Map<String, DocumentationOrigin> ruleDocOrigin = new HashMap<>(); | 
|  |  | 
|  | // Set of files already processed. The same file may be encountered multiple times because | 
|  | // directories are processed recursively, and an input directory may be a subdirectory of | 
|  | // another one. | 
|  | Set<File> processedFiles = new HashSet<>(); | 
|  |  | 
|  | for (String inputJavaDir : inputJavaDirs) { | 
|  | logger.atFine().log("Processing input directory: %s", inputJavaDir); | 
|  | int ruleNum = ruleDocEntries.size(); | 
|  | collectJavaSourceDocs( | 
|  | processedFiles, | 
|  | ruleDocOrigin, | 
|  | ruleDocEntries, | 
|  | denylistedRules, | 
|  | attributeDocEntries, | 
|  | new File(inputJavaDir)); | 
|  | logger.atFine().log( | 
|  | "%d rule documentations found in %s", ruleDocEntries.size() - ruleNum, inputJavaDir); | 
|  | } | 
|  | processJavaSourceRuleAttributeDocs(ruleDocEntries.values(), attributeDocEntries); | 
|  |  | 
|  | for (String stardocProtoPath : inputStardocProtos) { | 
|  | logger.atFine().log("Processing input file: %s", stardocProtoPath); | 
|  | int numRulesCollected = | 
|  | collectModuleInfoDocs( | 
|  | ruleDocOrigin, | 
|  | ruleDocEntries, | 
|  | denylistedRules, | 
|  | attributeDocEntries, | 
|  | ModuleInfo.parseFrom( | 
|  | new FileInputStream(stardocProtoPath), ExtensionRegistry.getEmptyRegistry()), | 
|  | urlMapper); | 
|  | logger.atFine().log( | 
|  | "%d rule documentations found in %s", numRulesCollected, stardocProtoPath); | 
|  | } | 
|  |  | 
|  | linkExpander.addIndex(buildRuleIndex(ruleDocEntries.values())); | 
|  | for (RuleDocumentation rule : ruleDocEntries.values()) { | 
|  | rule.setRuleLinkExpander(linkExpander); | 
|  | } | 
|  | return ruleDocEntries; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Generates an index mapping rule name to its normalized rule family name. | 
|  | */ | 
|  | private Map<String, String> buildRuleIndex(Iterable<RuleDocumentation> rules) { | 
|  | Map<String, String> index = new HashMap<>(); | 
|  | for (RuleDocumentation rule : rules) { | 
|  | index.put(rule.getRuleName(), RuleFamily.normalize(rule.getRuleFamily())); | 
|  | } | 
|  | return index; | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Go through all attributes of native rules whose documentation was retrieved from Java sources, | 
|  | * and search the best attribute documentation if exists. The best documentation is the closest | 
|  | * documentation in the ancestor graph. E.g. if java_library.deps documented in $rule and | 
|  | * $java_rule then the one in $java_rule is going to apply since it's a closer ancestor of | 
|  | * java_library. | 
|  | * | 
|  | * <p>Note: this function should be called before any calls to collectModuleInfoDocs. | 
|  | */ | 
|  | private void processJavaSourceRuleAttributeDocs( | 
|  | Iterable<RuleDocumentation> ruleDocEntries, | 
|  | ListMultimap<String, RuleDocumentationAttribute> attributeDocEntries) | 
|  | throws BuildEncyclopediaDocException { | 
|  | for (RuleDocumentation ruleDoc : ruleDocEntries) { | 
|  | RuleClass ruleClass = ruleClassProvider.getRuleClassMap().get(ruleDoc.getRuleName()); | 
|  | if (ruleClass != null) { | 
|  | if (ruleClass.isDocumented()) { | 
|  | Class<? extends RuleDefinition> ruleDefinition = | 
|  | ruleClassProvider.getRuleClassDefinition(ruleDoc.getRuleName()).getClass(); | 
|  | for (Attribute attribute : ruleClass.getAttributes()) { | 
|  | if (!attribute.isDocumented()) { | 
|  | continue; | 
|  | } | 
|  | String attrName = attribute.getName(); | 
|  | List<RuleDocumentationAttribute> attributeDocList = | 
|  | attributeDocEntries.get(attrName); | 
|  | if (attributeDocList != null) { | 
|  | // There are attribute docs for this attribute. | 
|  | // Search the closest one in the ancestor graph. | 
|  | // Note that there can be only one 'closest' attribute since we forbid multiple | 
|  | // inheritance of the same attribute in RuleClass. | 
|  | int minLevel = Integer.MAX_VALUE; | 
|  | RuleDocumentationAttribute bestAttributeDoc = null; | 
|  | for (RuleDocumentationAttribute attributeDoc : attributeDocList) { | 
|  | int level = attributeDoc.getDefinitionClassAncestryLevel( | 
|  | ruleDefinition, | 
|  | ruleClassProvider); | 
|  | if (level >= 0 && level < minLevel) { | 
|  | bestAttributeDoc = attributeDoc; | 
|  | minLevel = level; | 
|  | } | 
|  | } | 
|  | if (bestAttributeDoc != null) { | 
|  | // We have to copy the matching RuleDocumentationAttribute here so that we don't | 
|  | // overwrite the reference to the actual attribute later by another attribute with | 
|  | // the same ancestor but different default values. | 
|  | ruleDoc.addAttribute(bestAttributeDoc.copyAndUpdateFrom(attribute)); | 
|  | // If there is no matching attribute doc try to add the common. | 
|  | } else if (ruleDoc.getRuleType().equals(RuleType.BINARY) | 
|  | && PredefinedAttributes.BINARY_ATTRIBUTES.containsKey(attrName)) { | 
|  | ruleDoc.addAttribute(PredefinedAttributes.BINARY_ATTRIBUTES.get(attrName)); | 
|  | } else if (ruleDoc.getRuleType().equals(RuleType.TEST) | 
|  | && PredefinedAttributes.TEST_ATTRIBUTES.containsKey(attrName)) { | 
|  | ruleDoc.addAttribute(PredefinedAttributes.TEST_ATTRIBUTES.get(attrName)); | 
|  | } else if (PredefinedAttributes.COMMON_ATTRIBUTES.containsKey(attrName)) { | 
|  | ruleDoc.addAttribute(PredefinedAttributes.COMMON_ATTRIBUTES.get(attrName)); | 
|  | } else if (PredefinedAttributes.TYPICAL_ATTRIBUTES.containsKey(attrName)) { | 
|  | ruleDoc.addAttribute(PredefinedAttributes.TYPICAL_ATTRIBUTES.get(attrName)); | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  | } else { | 
|  | throw ruleDoc.createException("Can't find RuleClass for " + ruleDoc.getRuleName()); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Crawls the specified inputPath and collects the raw rule and rule attribute documentation. | 
|  | * | 
|  | * <p>This method crawls the specified input directory (recursively calling itself for all | 
|  | * subdirectories) and reads each Java source file using {@link SourceFileReader} to extract the | 
|  | * raw rule and attribute documentation embedded in comments in a specific format. The extracted | 
|  | * documentation is then further processed, such as by {@link | 
|  | * BuildDocCollector#collect(List<String>, String, RuleLinkExpander), collect}, in order to | 
|  | * associate each rule's documentation with its attribute documentation. | 
|  | * | 
|  | * <p>This method returns the following through its parameters: the set of Java source files | 
|  | * processed, a map of rule name to the source file it was extracted from, a map of rule name to | 
|  | * the documentation to the rule, and a multimap of attribute name to attribute documentation. | 
|  | * | 
|  | * @param processedFiles The set of Java source files files that have already been processed in | 
|  | *     order to avoid reprocessing the same file. | 
|  | * @param ruleDocOrigin Map of rule name to the file and symbol from which its documentation was | 
|  | *     obtained. | 
|  | * @param ruleDocEntries Map of rule name to rule documentation. | 
|  | * @param denyList The set of denylisted rules whose documentation should not be extracted. | 
|  | * @param attributeDocEntries Multimap of rule attribute name to attribute documentation. | 
|  | * @param inputPath The File representing the Java source file or directory to read. | 
|  | * @throws BuildEncyclopediaDocException | 
|  | * @throws IOException | 
|  | */ | 
|  | public void collectJavaSourceDocs( | 
|  | Set<File> processedFiles, | 
|  | Map<String, DocumentationOrigin> ruleDocOrigin, | 
|  | Map<String, RuleDocumentation> ruleDocEntries, | 
|  | Set<String> denyList, | 
|  | ListMultimap<String, RuleDocumentationAttribute> attributeDocEntries, | 
|  | File inputPath) | 
|  | throws BuildEncyclopediaDocException, IOException { | 
|  | if (processedFiles.contains(inputPath)) { | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (inputPath.isFile()) { | 
|  | if (DocgenConsts.JAVA_SOURCE_FILE_SUFFIX.apply(inputPath.getName())) { | 
|  | SourceFileReader sfr = | 
|  | new SourceFileReader( | 
|  | ruleClassProvider, inputPath.getAbsolutePath(), urlMapper.urlOfFile(inputPath)); | 
|  | sfr.readDocsFromComments(); | 
|  | for (RuleDocumentation d : sfr.getRuleDocEntries()) { | 
|  | String ruleName = d.getRuleName(); | 
|  | if (!denyList.contains(ruleName)) { | 
|  | if (ruleDocEntries.containsKey(ruleName) | 
|  | && !ruleDocOrigin.get(ruleName).file().equals(inputPath.toString())) { | 
|  | logger.atWarning().log( | 
|  | "Rule '%s' from '%s' overrides previously seen rule '%s' from '%s'", | 
|  | ruleName, | 
|  | inputPath, | 
|  | ruleDocOrigin.get(ruleName).symbol(), | 
|  | ruleDocOrigin.get(ruleName).file()); | 
|  | } | 
|  | ruleDocOrigin.put(ruleName, DocumentationOrigin.create(inputPath.toString(), ruleName)); | 
|  | ruleDocEntries.put(ruleName, d); | 
|  | } | 
|  | } | 
|  | if (attributeDocEntries != null) { | 
|  | // Collect all attribute documentations from this file. | 
|  | attributeDocEntries.putAll(sfr.getAttributeDocEntries()); | 
|  | } | 
|  | } | 
|  | } else if (inputPath.isDirectory()) { | 
|  | for (File childPath : inputPath.listFiles()) { | 
|  | collectJavaSourceDocs( | 
|  | processedFiles, | 
|  | ruleDocOrigin, | 
|  | ruleDocEntries, | 
|  | denyList, | 
|  | attributeDocEntries, | 
|  | childPath); | 
|  | } | 
|  | } | 
|  |  | 
|  | processedFiles.add(inputPath); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Collects rule and rule attribute documentation from a stardoc_output.ModuleInfo message | 
|  | * generated from a Build Encyclopedia entry point .bzl file. | 
|  | * | 
|  | * <p>The module doc string for the .bzl file is interpreted as the rule family name. | 
|  | * | 
|  | * <p>Any rule exported by the .bzl file is expected to be contained in a struct whose name is a | 
|  | * {@link DocgenConsts.RuleType} name suffixed with "_rules" - for example, "binary_rules", | 
|  | * "library_rules", etc. | 
|  | * | 
|  | * <p>This method returns the following through its parameters: a map of rule name to the file and | 
|  | * symbol it was extracted from, a map of rule name to the documentation of the rule, and a | 
|  | * multimap of attribute name to attribute documentation. | 
|  | * | 
|  | * @param ruleDocOrigin Map of rule name to the file and symbol from which its documentation was | 
|  | *     obtained. | 
|  | * @param ruleDocEntries Map of rule name to rule documentation. | 
|  | * @param denyList The set of denylisted rules whose documentation should not be extracted. | 
|  | * @param attributeDocEntries Multimap of rule attribute name to attribute documentation. | 
|  | * @param moduleInfo A stardoc_output.ModuleInfo message representing a Build Encyclopedia entry | 
|  | *     point .bzl file. | 
|  | * @param urlMapper Mapper from source labels to source code repository URLs | 
|  | * @return number of rules whose documentation was collected | 
|  | */ | 
|  | @VisibleForTesting | 
|  | static int collectModuleInfoDocs( | 
|  | Map<String, DocumentationOrigin> ruleDocOrigin, | 
|  | Map<String, RuleDocumentation> ruleDocEntries, | 
|  | Set<String> denyList, | 
|  | ListMultimap<String, RuleDocumentationAttribute> attributeDocEntries, | 
|  | ModuleInfo moduleInfo, | 
|  | SourceUrlMapper urlMapper) | 
|  | throws BuildEncyclopediaDocException { | 
|  | String entryPointFileLabel = moduleInfo.getFile(); | 
|  |  | 
|  | Matcher familyMatcher = | 
|  | DocgenConsts.STARDOC_OUTPUT_FAMILY_NAME_AND_SUMMARY.matcher( | 
|  | moduleInfo.getModuleDocstring().strip()); | 
|  | if (!familyMatcher.matches()) { | 
|  | throw new BuildEncyclopediaDocException( | 
|  | entryPointFileLabel, | 
|  | "Module doc string is expected to be a single line representing a rule family name, " | 
|  | + "optionally followed by a blank line and summary text; for example, " | 
|  | + "`\"\"\"C / C++\"\"\"`"); | 
|  | } | 
|  | String ruleFamily = familyMatcher.group("family"); | 
|  | String ruleFamilySummary = Strings.nullToEmpty(familyMatcher.group("summary")); | 
|  |  | 
|  | int numRulesCollected = 0; | 
|  | for (RuleInfo ruleInfo : moduleInfo.getRuleInfoList()) { | 
|  | Matcher ruleNameMatcher = | 
|  | DocgenConsts.STARDOC_OUTPUT_RULE_NAME.matcher(ruleInfo.getRuleName()); | 
|  | if (!ruleNameMatcher.matches()) { | 
|  | throw new BuildEncyclopediaDocException( | 
|  | entryPointFileLabel, | 
|  | String.format( | 
|  | "Unexpected rule symbol: %s; rules must be exported in structs, with the struct's" | 
|  | + " name specifying the rule type, for example, `library_rules = struct(" | 
|  | + "java_import = _java_import, ...)`", | 
|  | ruleInfo.getRuleName())); | 
|  | } | 
|  | String ruleType = Ascii.toUpperCase(ruleNameMatcher.group("type")); | 
|  | String ruleName = ruleNameMatcher.group("name"); | 
|  | if (!denyList.contains(ruleName)) { | 
|  | String ruleOriginFileLabel = ruleInfo.getOriginKey().getFile(); | 
|  | if (ruleDocEntries.containsKey(ruleName)) { | 
|  | logger.atWarning().log( | 
|  | "Rule '%s' from '%s' (defined in '%s') overrides previously seen rule '%s' from '%s'", | 
|  | ruleInfo.getRuleName(), | 
|  | entryPointFileLabel, | 
|  | ruleOriginFileLabel, | 
|  | ruleDocOrigin.get(ruleName).symbol(), | 
|  | ruleDocOrigin.get(ruleName).file()); | 
|  | } | 
|  | ImmutableSet.Builder<String> flags = ImmutableSet.builder(); | 
|  | if (ruleType.equals(DocgenConsts.STARLARK_GENERIC_RULE_TYPE)) { | 
|  | // Note that if FLAG_GENERIC_RULE is set, RuleDocumentation constructor will set the rule | 
|  | // type to OTHER. | 
|  | flags.add(DocgenConsts.FLAG_GENERIC_RULE); | 
|  | } | 
|  | RuleDocumentation ruleDoc = | 
|  | new RuleDocumentation( | 
|  | ruleName, | 
|  | ruleType, | 
|  | ruleFamily, | 
|  | ruleInfo.getDocString(), | 
|  | ruleOriginFileLabel, | 
|  | urlMapper.urlOfLabel(ruleOriginFileLabel), | 
|  | flags.build(), | 
|  | // Add family summary only to the first rule encountered, to avoid duplication in | 
|  | // final rendered output | 
|  | numRulesCollected == 0 ? ruleFamilySummary : ""); | 
|  |  | 
|  | // Inject standard inherited attributes for Starlark rules (since they always inherit from | 
|  | // one of 3 possible base rule classes; see StarlarkRuleClassFunctions#createRule). If in | 
|  | // the future we want to document native rules via ModuleInfo protos, we will need to list | 
|  | // inherited attributes in the proto. | 
|  | ruleDoc.addAttributes(PredefinedAttributes.COMMON_ATTRIBUTES.values()); | 
|  | if (ruleDoc.getRuleType().equals(RuleType.TEST)) { | 
|  | ruleDoc.addAttributes(PredefinedAttributes.TEST_ATTRIBUTES.values()); | 
|  | } else if (ruleDoc.getRuleType().equals(RuleType.BINARY)) { | 
|  | ruleDoc.addAttributes(PredefinedAttributes.BINARY_ATTRIBUTES.values()); | 
|  | } | 
|  |  | 
|  | for (AttributeInfo attributeInfo : ruleInfo.getAttributeList()) { | 
|  | String attributeName = attributeInfo.getName(); | 
|  | if (attributeName.equals("name")) { | 
|  | // We do not want the implicit "name" attribute injected into proto output by | 
|  | // starlark_doc_extract because we inject "name" at the template level in | 
|  | // templates/be/rules.vm | 
|  | continue; | 
|  | } | 
|  | if (attributeInfo.getDocString().isEmpty() | 
|  | && PredefinedAttributes.TYPICAL_ATTRIBUTES.containsKey(attributeName)) { | 
|  | // We link empty-docstring attributes to the common table based purely on attribute name | 
|  | // (same as processJavaSourceRuleAttributeDocs does for native rule attributes). | 
|  | // TODO(arostovtsev): should we verify attribute type and default value too? That would | 
|  | // require moving the definition of common attributes from a free-text velocity template | 
|  | // to a structured format. | 
|  | ruleDoc.addAttribute(PredefinedAttributes.TYPICAL_ATTRIBUTES.get(attributeName)); | 
|  | } else { | 
|  | boolean deprecated = | 
|  | DocgenConsts.STARDOC_OUTPUT_DEPRECATED_DOCSTRING | 
|  | .matcher(attributeInfo.getDocString()) | 
|  | .find(); | 
|  | ruleDoc.addAttribute( | 
|  | RuleDocumentationAttribute.createFromAttributeInfo( | 
|  | attributeInfo, | 
|  | ruleOriginFileLabel, | 
|  | deprecated | 
|  | ? ImmutableSet.of(DocgenConsts.FLAG_DEPRECATED) | 
|  | : ImmutableSet.of())); | 
|  | } | 
|  | } | 
|  |  | 
|  | ruleDocOrigin.put( | 
|  | ruleName, DocumentationOrigin.create(entryPointFileLabel, ruleInfo.getRuleName())); | 
|  | ruleDocEntries.put(ruleName, ruleDoc); | 
|  | numRulesCollected++; | 
|  | } | 
|  | } | 
|  | return numRulesCollected; | 
|  | } | 
|  |  | 
|  | /** The file and symbol from which documentation was obtained. */ | 
|  | @AutoValue | 
|  | abstract static class DocumentationOrigin { | 
|  | abstract String file(); | 
|  |  | 
|  | abstract String symbol(); | 
|  |  | 
|  | static DocumentationOrigin create(String file, String symbol) { | 
|  | return new AutoValue_BuildDocCollector_DocumentationOrigin(file, symbol); | 
|  | } | 
|  | } | 
|  | } |