// 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.base.Predicate; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Ordering; import com.google.common.collect.Sets; 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.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; 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.TreeSet; /** * A class to assemble documentation for the Build Encyclopedia. The * program parses the documentation fragments of rule-classes and * generates the html format documentation. */ public class BuildEncyclopediaProcessor { private static final Predicate RULE_WORTH_DOCUMENTING = new Predicate() { @Override public boolean apply(String name) { return !name.contains("$"); } }; private ConfiguredRuleClassProvider ruleClassProvider; /** * Creates the BuildEncyclopediaProcessor instance. The ruleClassProvider parameter * is used for rule class hierarchy and attribute checking. * */ public BuildEncyclopediaProcessor(ConfiguredRuleClassProvider ruleClassProvider) { this.ruleClassProvider = Preconditions.checkNotNull(ruleClassProvider); } /** * Collects and processes all the rule and attribute documentation in inputDirs and * generates the Build Encyclopedia into the outputRootDir. */ public void generateDocumentation(String[] inputDirs, String outputRootDir) throws BuildEncyclopediaDocException, IOException { File buildEncyclopediaPath = setupDirectories(outputRootDir); try (BufferedWriter bw = new BufferedWriter(new FileWriter(buildEncyclopediaPath))) { bw.write(DocgenConsts.HEADER_COMMENT); bw.write("\n"); // for the benefit of the block-beginning comment at the top of the template Map ruleDocEntries = collectAndProcessRuleDocs(inputDirs, false); warnAboutUndocumentedRules( Sets.difference(ruleClassProvider.getRuleClassMap().keySet(), ruleDocEntries.keySet())); writeRuleClassDocs(ruleDocEntries.values(), bw); bw.write("\n"); // for the benefit of the block-beginning comment at the top of the template bw.write(SourceFileReader.readTemplateContents(DocgenConsts.FOOTER_TEMPLATE)); } } /** * Collects all the rule and attribute documentation present in inputDirs, integrates the * attribute documentation in the rule documentation and returns the rule documentation. */ public Map collectAndProcessRuleDocs(String[] inputDirs, boolean printMessages) throws BuildEncyclopediaDocException, IOException { // 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 ruleDocEntries = new TreeMap<>(); // RuleDocumentationAttribute objects equal based on attributeName so they have to be // collected in a List instead of a Set. ListMultimap attributeDocEntries = LinkedListMultimap.create(); // Map of rule class name to file that defined it. Map 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 processedFiles = new HashSet<>(); for (String inputDir : inputDirs) { if (printMessages) { System.out.println(" Processing input directory: " + inputDir); } int ruleNum = ruleDocEntries.size(); collectDocs(processedFiles, ruleClassFiles, ruleDocEntries, attributeDocEntries, new File(inputDir)); if (printMessages) { System.out.println( " " + (ruleDocEntries.size() - ruleNum) + " rule documentations found."); } } processAttributeDocs(ruleDocEntries.values(), attributeDocEntries); return ruleDocEntries; } /** * 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 ruleDocEntries, ListMultimap attributeDocEntries) throws BuildEncyclopediaDocException { for (RuleDocumentation ruleDoc : ruleDocEntries) { RuleClass ruleClass = ruleClassProvider.getRuleClassMap().get(ruleDoc.getRuleName()); if (ruleClass != null) { if (ruleClass.isDocumented()) { Class ruleDefinition = ruleClassProvider.getRuleClassDefinition(ruleDoc.getRuleName()); for (Attribute attribute : ruleClass.getAttributes()) { String attrName = attribute.getName(); List 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); if (level >= 0 && level < minLevel) { bestAttributeDoc = attributeDoc; minLevel = level; } } if (bestAttributeDoc != null) { 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()); } } } /** * Categorizes, checks and prints all the rule-class documentations. */ private void writeRuleClassDocs(Iterable docEntries, BufferedWriter bw) throws BuildEncyclopediaDocException, IOException { Set binaryDocs = new TreeSet<>(); Set libraryDocs = new TreeSet<>(); Set testDocs = new TreeSet<>(); Set otherDocs = new TreeSet<>(); for (RuleDocumentation doc : docEntries) { RuleClass ruleClass = ruleClassProvider.getRuleClassMap().get(doc.getRuleName()); if (!ruleClass.isDocumented()) { continue; } if (doc.isLanguageSpecific()) { switch(doc.getRuleType()) { case BINARY: binaryDocs.add(doc); break; case LIBRARY: libraryDocs.add(doc); break; case TEST: testDocs.add(doc); break; case OTHER: otherDocs.add(doc); break; } } else { otherDocs.add(doc); } } bw.write("\n"); // for the benefit of the block-beginning comment at the top of the template bw.write(SourceFileReader.readTemplateContents(DocgenConsts.HEADER_TEMPLATE, generateBEHeaderMapping(docEntries))); Map sectionMapping = ImmutableMap.of( DocgenConsts.VAR_SECTION_BINARY, getRuleDocs(binaryDocs), DocgenConsts.VAR_SECTION_LIBRARY, getRuleDocs(libraryDocs), DocgenConsts.VAR_SECTION_TEST, getRuleDocs(testDocs), DocgenConsts.VAR_SECTION_OTHER, getRuleDocs(otherDocs)); bw.write("\n"); // for the benefit of the block-beginning comment at the top of the template bw.write(SourceFileReader.readTemplateContents(DocgenConsts.BODY_TEMPLATE, sectionMapping)); } private Map generateBEHeaderMapping(Iterable docEntries) throws BuildEncyclopediaDocException { StringBuilder sb = new StringBuilder(); sb.append("\n") .append("\n") .append("" + "\n"); // Separate rule families into language-specific and generic ones. Set languageSpecificRuleFamilies = new TreeSet<>(); Set genericRuleFamilies = new TreeSet<>(); separateRuleFamilies(docEntries, languageSpecificRuleFamilies, genericRuleFamilies); // Create a mapping of rules based on rule type and family. Map> ruleMapping = new HashMap<>(); createRuleMapping(docEntries, ruleMapping); // Generate the table. for (String ruleFamily : languageSpecificRuleFamilies) { generateHeaderTableRuleFamily(sb, ruleMapping.get(ruleFamily), ruleFamily); } sb.append(""); sb.append(""); for (String ruleFamily : genericRuleFamilies) { generateHeaderTableRuleFamily(sb, ruleMapping.get(ruleFamily), ruleFamily); } sb.append("
LanguageBinary rulesLibrary rulesTest rulesOther rules
 
Rules that do not apply to a " + "specific programming language
\n"); return ImmutableMap.of(DocgenConsts.VAR_HEADER_TABLE, sb.toString(), DocgenConsts.VAR_COMMON_ATTRIBUTE_DEFINITION, generateCommonAttributeDocs( PredefinedAttributes.COMMON_ATTRIBUTES, DocgenConsts.COMMON_ATTRIBUTES), DocgenConsts.VAR_TEST_ATTRIBUTE_DEFINITION, generateCommonAttributeDocs( PredefinedAttributes.TEST_ATTRIBUTES, DocgenConsts.TEST_ATTRIBUTES), DocgenConsts.VAR_BINARY_ATTRIBUTE_DEFINITION, generateCommonAttributeDocs( PredefinedAttributes.BINARY_ATTRIBUTES, DocgenConsts.BINARY_ATTRIBUTES), DocgenConsts.VAR_LEFT_PANEL, generateLeftNavigationPanel(docEntries)); } /** * Create a mapping of rules based on rule type and family. */ private void createRuleMapping(Iterable docEntries, Map> ruleMapping) throws BuildEncyclopediaDocException { for (RuleDocumentation ruleDoc : docEntries) { RuleClass ruleClass = ruleClassProvider.getRuleClassMap().get(ruleDoc.getRuleName()); if (ruleClass != null) { String ruleFamily = ruleDoc.getRuleFamily(); if (!ruleMapping.containsKey(ruleFamily)) { ruleMapping.put(ruleFamily, LinkedListMultimap.create()); } if (ruleClass.isDocumented()) { ruleMapping.get(ruleFamily).put(ruleDoc.getRuleType(), ruleDoc); } } else { throw ruleDoc.createException("Can't find RuleClass for " + ruleDoc.getRuleName()); } } } /** * Separates all rule families in docEntries into language-specific rules and generic rules. */ private void separateRuleFamilies(Iterable docEntries, Set languageSpecificRuleFamilies, Set genericRuleFamilies) throws BuildEncyclopediaDocException { for (RuleDocumentation ruleDoc : docEntries) { if (ruleDoc.isLanguageSpecific()) { if (genericRuleFamilies.contains(ruleDoc.getRuleFamily())) { throw ruleDoc.createException("The rule is marked as being language-specific, but other " + "rules of the same family have already been marked as being not."); } languageSpecificRuleFamilies.add(ruleDoc.getRuleFamily()); } else { if (languageSpecificRuleFamilies.contains(ruleDoc.getRuleFamily())) { throw ruleDoc.createException("The rule is marked as being generic, but other rules of " + "the same family have already been marked as being language-specific."); } genericRuleFamilies.add(ruleDoc.getRuleFamily()); } } } private String generateLeftNavigationPanel(Iterable docEntries) { // Order the rules alphabetically. At this point they are ordered according to // RuleDocumentation.compareTo() which is not alphabetical. TreeMap ruleNames = new TreeMap<>(); for (RuleDocumentation ruleDoc : docEntries) { String ruleName = ruleDoc.getRuleName(); ruleNames.put(ruleName.toLowerCase(), ruleName); } StringBuilder sb = new StringBuilder(); for (String ruleName : ruleNames.values()) { RuleClass ruleClass = ruleClassProvider.getRuleClassMap().get(ruleName); Preconditions.checkNotNull(ruleClass); if (ruleClass.isDocumented()) { sb.append(String.format("%s
\n", ruleName, ruleName)); } } return sb.toString(); } private String generateCommonAttributeDocs(Map attributes, String attributeGroupName) throws BuildEncyclopediaDocException { RuleDocumentation ruleDoc = new RuleDocumentation( attributeGroupName, "OTHER", null, null, 0, null, ImmutableSet.of(), ruleClassProvider); for (RuleDocumentationAttribute attribute : attributes.values()) { ruleDoc.addAttribute(attribute); } return ruleDoc.generateAttributeDefinitions(); } private void generateHeaderTableRuleFamily(StringBuilder sb, ListMultimap ruleTypeMap, String ruleFamily) { sb.append("\n") .append(String.format("%s\n", ruleFamily)); boolean otherRulesSplitted = false; for (RuleType ruleType : DocgenConsts.RuleType.values()) { sb.append(""); int i = 0; List ruleDocList = ruleTypeMap.get(ruleType); for (RuleDocumentation ruleDoc : ruleDocList) { if (i > 0) { if (ruleType.equals(RuleType.OTHER) && ruleDocList.size() >= 4 && i == (ruleDocList.size() + 1) / 2) { // Split 'other rules' into two columns if there are too many of them. sb.append("\n"); otherRulesSplitted = true; } else { sb.append("
"); } } String ruleName = ruleDoc.getRuleName(); String deprecatedString = ruleDoc.hasFlag(DocgenConsts.FLAG_DEPRECATED) ? " class=\"deprecated\"" : ""; sb.append(String.format("%s", ruleName, deprecatedString, ruleName)); i++; } sb.append("\n"); } // There should be 6 columns. if (!otherRulesSplitted) { sb.append("\n"); } sb.append("\n"); } private String getRuleDocs(Iterable docEntries) { StringBuilder sb = new StringBuilder(); for (RuleDocumentation doc : docEntries) { sb.append(doc.getHtmlDocumentation()); } return sb.toString(); } /** * Goes through all the html files and subdirs under inputPath and collects the rule * and attribute documentations using the ruleDocEntries and attributeDocEntries variable. */ public void collectDocs( Set processedFiles, Map ruleClassFiles, Map ruleDocEntries, ListMultimap 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 (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, attributeDocEntries, childPath); } } processedFiles.add(inputPath); } private File setupDirectories(String outputRootDir) { if (outputRootDir != null) { File outputRootPath = new File(outputRootDir); outputRootPath.mkdirs(); return new File(outputRootDir + File.separator + DocgenConsts.BUILD_ENCYCLOPEDIA_NAME); } else { return new File(DocgenConsts.BUILD_ENCYCLOPEDIA_NAME); } } private static void warnAboutUndocumentedRules(Iterable rulesWithoutDocumentation) { Iterable undocumentedRules = Iterables.filter(rulesWithoutDocumentation, RULE_WORTH_DOCUMENTING); System.err.printf("WARNING: The following rules are undocumented: [%s]\n", Joiner.on(", ").join(Ordering.natural().immutableSortedCopy(undocumentedRules))); } }