// 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.Splitter; import com.google.common.collect.ImmutableSet; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.ListMultimap; import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.regex.Matcher; /** * A helper class to read and process documentations for rule classes and attributes * from exactly one java source file. */ public class SourceFileReader { private Collection<RuleDocumentation> ruleDocEntries; private ListMultimap<String, RuleDocumentationAttribute> attributeDocEntries; private final ConfiguredRuleClassProvider ruleClassProvider; private final String javaSourceFilePath; public SourceFileReader( ConfiguredRuleClassProvider ruleClassProvider, String javaSourceFilePath) { this.ruleClassProvider = ruleClassProvider; this.javaSourceFilePath = javaSourceFilePath; } /** * The handler class of the line read from the text file. */ public abstract static class ReadAction { // Text file line indexing starts from 1 private int lineCnt = 1; protected abstract void readLineImpl(String line) throws BuildEncyclopediaDocException, IOException; protected int getLineCnt() { return lineCnt; } public void readLine(String line) throws BuildEncyclopediaDocException, IOException { readLineImpl(line); lineCnt++; } } private static final String LS = DocgenConsts.LS; /** * Reads the attribute and rule documentation present in the file represented by * SourceFileReader.javaSourceFilePath. The rule doc variables are added to the rule * documentation (which therefore must be defined in the same file). The attribute docs are * stored in a different class member, so they need to be handled outside this method. */ public void readDocsFromComments() throws BuildEncyclopediaDocException, IOException { final Map<String, RuleDocumentation> docMap = new HashMap<>(); final List<RuleDocumentationVariable> docVariables = new LinkedList<>(); final ListMultimap<String, RuleDocumentationAttribute> docAttributes = LinkedListMultimap.create(); readTextFile(javaSourceFilePath, new ReadAction() { private boolean inBlazeRuleDocs = false; private boolean inBlazeRuleVarDocs = false; private boolean inBlazeAttributeDocs = false; private StringBuilder sb = new StringBuilder(); private String ruleName; private String ruleType; private String ruleFamily; private String variableName; private String attributeName; private ImmutableSet<String> flags; private int startLineCnt; @Override public void readLineImpl(String line) throws BuildEncyclopediaDocException { // TODO(bazel-team): check if copy paste code can be reduced using inner classes if (inBlazeRuleDocs) { if (DocgenConsts.BLAZE_RULE_END.matcher(line).matches()) { endBlazeRuleDoc(docMap); } else { appendLine(line); } } else if (inBlazeRuleVarDocs) { if (DocgenConsts.BLAZE_RULE_VAR_END.matcher(line).matches()) { endBlazeRuleVarDoc(docVariables); } else { appendLine(line); } } else if (inBlazeAttributeDocs) { if (DocgenConsts.BLAZE_RULE_ATTR_END.matcher(line).matches()) { endBlazeAttributeDoc(docAttributes); } else { appendLine(line); } } Matcher ruleStartMatcher = DocgenConsts.BLAZE_RULE_START.matcher(line); Matcher ruleVarStartMatcher = DocgenConsts.BLAZE_RULE_VAR_START.matcher(line); Matcher ruleAttrStartMatcher = DocgenConsts.BLAZE_RULE_ATTR_START.matcher(line); if (ruleStartMatcher.find()) { startBlazeRuleDoc(line, ruleStartMatcher); } else if (ruleVarStartMatcher.find()) { startBlazeRuleVarDoc(ruleVarStartMatcher); } else if (ruleAttrStartMatcher.find()) { startBlazeAttributeDoc(line, ruleAttrStartMatcher); } } private void appendLine(String line) { // Add another line of html code to the building rule documentation // Removing whitespace and java comment asterisk from the beginning of the line sb.append(line.replaceAll("^[\\s]*\\*", "") + LS); } private void startBlazeRuleDoc(String line, Matcher matcher) throws BuildEncyclopediaDocException { checkDocValidity(); // Start of a new rule. // e.g.: matcher.group(1) = "NAME = cc_binary, TYPE = BINARY, FAMILY = C / C++" for (String group : Splitter.on(",").split(matcher.group(1))) { List<String> parts = Splitter.on("=").limit(2).splitToList(group); boolean good = false; if (parts.size() == 2) { String key = parts.get(0).trim(); String value = parts.get(1).trim(); good = true; if (DocgenConsts.META_KEY_NAME.equals(key)) { ruleName = value; } else if (DocgenConsts.META_KEY_TYPE.equals(key)) { ruleType = value; } else if (DocgenConsts.META_KEY_FAMILY.equals(key)) { ruleFamily = value; } else { good = false; } } if (!good) { System.err.printf("WARNING: bad rule definition in line %d: '%s'", getLineCnt(), line); } } startLineCnt = getLineCnt(); addFlags(line); inBlazeRuleDocs = true; } private void endBlazeRuleDoc(final Map<String, RuleDocumentation> documentations) throws BuildEncyclopediaDocException { // End of a rule, create RuleDocumentation object documentations.put(ruleName, new RuleDocumentation(ruleName, ruleType, ruleFamily, sb.toString(), getLineCnt(), javaSourceFilePath, flags, ruleClassProvider)); sb = new StringBuilder(); inBlazeRuleDocs = false; } private void startBlazeRuleVarDoc(Matcher matcher) throws BuildEncyclopediaDocException { checkDocValidity(); // Start of a new rule variable ruleName = matcher.group(1).replaceAll("[\\s]", ""); variableName = matcher.group(2).replaceAll("[\\s]", ""); startLineCnt = getLineCnt(); inBlazeRuleVarDocs = true; } private void endBlazeRuleVarDoc(final List<RuleDocumentationVariable> docVariables) { // End of a rule, create RuleDocumentationVariable object docVariables.add( new RuleDocumentationVariable(ruleName, variableName, sb.toString(), startLineCnt)); sb = new StringBuilder(); inBlazeRuleVarDocs = false; } private void startBlazeAttributeDoc(String line, Matcher matcher) throws BuildEncyclopediaDocException { checkDocValidity(); // Start of a new attribute ruleName = matcher.group(1).replaceAll("[\\s]", ""); attributeName = matcher.group(2).replaceAll("[\\s]", ""); startLineCnt = getLineCnt(); addFlags(line); inBlazeAttributeDocs = true; } private void endBlazeAttributeDoc( final ListMultimap<String, RuleDocumentationAttribute> docAttributes) { // End of a attribute, create RuleDocumentationAttribute object docAttributes.put(attributeName, RuleDocumentationAttribute.create( ruleClassProvider.getRuleClassDefinition(ruleName).getClass(), attributeName, sb.toString(), startLineCnt, javaSourceFilePath, flags)); sb = new StringBuilder(); inBlazeAttributeDocs = false; } private void addFlags(String line) { // Add flags if there's any Matcher matcher = DocgenConsts.BLAZE_RULE_FLAGS.matcher(line); if (matcher.find()) { flags = ImmutableSet.<String>copyOf(matcher.group(1).split(",")); } else { flags = ImmutableSet.<String>of(); } } private void checkDocValidity() throws BuildEncyclopediaDocException { if (inBlazeRuleDocs || inBlazeRuleVarDocs || inBlazeAttributeDocs) { throw new BuildEncyclopediaDocException(javaSourceFilePath, getLineCnt(), "Malformed documentation, #BLAZE_RULE started after another #BLAZE_RULE."); } } }); // Adding rule doc variables to the corresponding rules for (RuleDocumentationVariable docVariable : docVariables) { if (docMap.containsKey(docVariable.getRuleName())) { docMap.get(docVariable.getRuleName()).addDocVariable( docVariable.getVariableName(), docVariable.getValue()); } else { throw new BuildEncyclopediaDocException(javaSourceFilePath, docVariable.getStartLineCnt(), String.format("Malformed rule variable #BLAZE_RULE(%s).%s, rule %s not found in file.", docVariable.getRuleName(), docVariable.getVariableName(), docVariable.getRuleName())); } } ruleDocEntries = docMap.values(); attributeDocEntries = docAttributes; } public Collection<RuleDocumentation> getRuleDocEntries() { return ruleDocEntries; } public ListMultimap<String, RuleDocumentationAttribute> getAttributeDocEntries() { return attributeDocEntries; } /** * Reads the template file without variable substitution. */ public static String readTemplateContents(String templateFilePath) throws BuildEncyclopediaDocException, IOException { return readTemplateContents(templateFilePath, null); } /** * Reads a template file and substitutes variables of the format ${FOO}. * * @param variables keys are the possible variable names, e.g. "FOO", values are the substitutions * (can be null) */ public static String readTemplateContents(String templateFilePath, final Map<String, String> variables) throws BuildEncyclopediaDocException, IOException { final StringBuilder sb = new StringBuilder(); readTextFile(templateFilePath, new ReadAction() { @Override public void readLineImpl(String line) { sb.append(expandVariables(line, variables)).append(LS); } }); return sb.toString(); } private static String expandVariables(String line, Map<String, String> variables) { if (variables == null || line.indexOf("${") == -1) { return line; } for (Entry<String, String> variable : variables.entrySet()) { line = line.replace("${" + variable.getKey() + "}", variable.getValue()); } return line; } public static void readTextFile(String filePath, ReadAction action) throws BuildEncyclopediaDocException, IOException { BufferedReader br = null; try { File file = new File(filePath); if (file.exists()) { br = new BufferedReader(new FileReader(file)); } else { InputStream is = SourceFileReader.class.getResourceAsStream(filePath); if (is != null) { br = new BufferedReader(new InputStreamReader(is)); } } if (br != null) { String line = null; while ((line = br.readLine()) != null) { action.readLine(line); } } else { System.out.println("Couldn't find file or resource: " + filePath); } } finally { if (br != null) { br.close(); } } } }