| // 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.common.annotations.VisibleForTesting; |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.LinkedListMultimap; |
| import com.google.common.collect.ListMultimap; |
| 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 java.io.BufferedReader; |
| import java.io.File; |
| 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; |
| |
| /** |
| * Class that parses the documentation fragments of rule-classes and |
| * generates the html format documentation. |
| */ |
| @VisibleForTesting |
| public class BuildDocCollector { |
| private static final Splitter SHARP_SPLITTER = Splitter.on('#').limit(2).trimResults(); |
| |
| private final String productName; |
| private final ConfiguredRuleClassProvider ruleClassProvider; |
| private final boolean printMessages; |
| |
| public BuildDocCollector( |
| String productName, ConfiguredRuleClassProvider ruleClassProvider, boolean printMessages) { |
| this.productName = productName; |
| this.ruleClassProvider = ruleClassProvider; |
| this.printMessages = printMessages; |
| } |
| |
| /** |
| * Parse the file containing blacklisted rules for 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 blackList The name of the file containing the blacklist. |
| * @return The set of blacklisted rules. |
| * @throws IOException |
| */ |
| @VisibleForTesting |
| public static Set<String> readBlackList(String blackList) throws IOException { |
| Set<String> result = new HashSet<String>(); |
| if (blackList != null && !blackList.isEmpty()) { |
| File file = new File(blackList); |
| 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 provided {@link RuleLinkExpander}. |
| * |
| * @param inputDirs list of directories to scan for documentation |
| * @param blackList specify an optional blacklist file that list some rules that should |
| * not be listed in the output. |
| * @param expander The RuleLinkExpander, which is used for expanding links in the rule doc. |
| * @throws BuildEncyclopediaDocException |
| * @throws IOException |
| * @return Map of rule class to rule documentation. |
| */ |
| public Map<String, RuleDocumentation> collect( |
| List<String> inputDirs, String blackList, RuleLinkExpander expander) |
| throws BuildEncyclopediaDocException, IOException { |
| // Read the blackList file |
| Set<String> blacklistedRules = readBlackList(blackList); |
| // 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 class name to file that defined it. |
| Map<String, File> ruleClassFiles = 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 inputDir : inputDirs) { |
| if (printMessages) { |
| System.out.println(" Processing input directory: " + inputDir); |
| } |
| int ruleNum = ruleDocEntries.size(); |
| collectDocs(processedFiles, ruleClassFiles, ruleDocEntries, blacklistedRules, |
| attributeDocEntries, new File(inputDir)); |
| if (printMessages) { |
| System.out.println(" " + (ruleDocEntries.size() - ruleNum) |
| + " rule documentations found."); |
| } |
| } |
| |
| processAttributeDocs(ruleDocEntries.values(), attributeDocEntries); |
| expander.addIndex(buildRuleIndex(ruleDocEntries.values())); |
| for (RuleDocumentation rule : ruleDocEntries.values()) { |
| rule.setRuleLinkExpander(expander); |
| } |
| return ruleDocEntries; |
| } |
| |
| /** |
| * 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 the multi-page Build Encyclopedia model (one page per rule class.). |
| * |
| * @param inputDirs list of directories to scan for documentation |
| * @param blackList specify an optional blacklist 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> inputDirs, String blackList) |
| throws BuildEncyclopediaDocException, IOException { |
| RuleLinkExpander expander = new RuleLinkExpander(productName, /* singlePage */ false); |
| return collect(inputDirs, blackList, expander); |
| } |
| |
| /** |
| * 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 all documented rules 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. |
| */ |
| private void processAttributeDocs(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) { |
| try { |
| // We have to clone 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. |
| bestAttributeDoc = (RuleDocumentationAttribute) bestAttributeDoc.clone(); |
| } catch (CloneNotSupportedException e) { |
| throw new BuildEncyclopediaDocException( |
| bestAttributeDoc.getFileName(), |
| bestAttributeDoc.getStartLineCnt(), |
| "attribute doesn't support clone: " + e.toString()); |
| } |
| // Add reference to the Attribute that the attribute doc is associated with |
| // in order to generate documentation for the Attribute. |
| bestAttributeDoc.setAttribute(attribute); |
| ruleDoc.addAttribute(bestAttributeDoc); |
| // 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 { |
| 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 ruleClassFiles Map of rule name to the source file it was extracted from. |
| * @param ruleDocEntries Map of rule name to rule documentation. |
| * @param blackList The set of blacklisted rules whose documentation should not be extracted. |
| * @param attributeDocEntries Multimap of rule attribute name to attribute documentation. |
| * @param inputPath The File representing the file or directory to read. |
| * @throws BuildEncyclopediaDocException |
| * @throws IOException |
| */ |
| public void collectDocs( |
| Set<File> processedFiles, |
| Map<String, File> ruleClassFiles, |
| Map<String, RuleDocumentation> ruleDocEntries, |
| Set<String> blackList, |
| 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()); |
| sfr.readDocsFromComments(); |
| for (RuleDocumentation d : sfr.getRuleDocEntries()) { |
| String ruleName = d.getRuleName(); |
| if (!blackList.contains(ruleName)) { |
| if (ruleDocEntries.containsKey(ruleName) |
| && !ruleClassFiles.get(ruleName).equals(inputPath)) { |
| System.err.printf( |
| "WARNING: '%s' from '%s' overrides value already in map from '%s'\n", |
| d.getRuleName(), inputPath, ruleClassFiles.get(ruleName)); |
| } |
| ruleClassFiles.put(ruleName, inputPath); |
| 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()) { |
| collectDocs(processedFiles, ruleClassFiles, ruleDocEntries, blackList, |
| attributeDocEntries, childPath); |
| } |
| } |
| |
| processedFiles.add(inputPath); |
| } |
| } |