|  | // 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.IOException; | 
|  | import java.io.InputStream; | 
|  | import java.io.InputStreamReader; | 
|  | import java.nio.charset.StandardCharsets; | 
|  | import java.nio.file.Files; | 
|  | import java.util.Collection; | 
|  | import java.util.HashMap; | 
|  | import java.util.LinkedList; | 
|  | import java.util.List; | 
|  | import java.util.Map; | 
|  | 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 boolean inFamilySummary = false; | 
|  | private StringBuilder sb = new StringBuilder(); | 
|  | private String ruleName; | 
|  | private String familySummary = ""; | 
|  | 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); | 
|  | } | 
|  | } else if (inFamilySummary) { | 
|  | if (DocgenConsts.FAMILY_SUMMARY_END.matcher(line).matches()) { | 
|  | endFamilySummary(); | 
|  | } else { | 
|  | appendLine(line); | 
|  | } | 
|  | } | 
|  | Matcher familySummaryStartMatcher = DocgenConsts.FAMILY_SUMMARY_START.matcher(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 (familySummaryStartMatcher.find()) { | 
|  | startFamilySummary(); | 
|  | } else 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 { | 
|  | sb = new StringBuilder(); | 
|  | 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 startFamilySummary() { | 
|  | sb = new StringBuilder(); | 
|  | inFamilySummary = true; | 
|  | } | 
|  |  | 
|  | private void endFamilySummary() { | 
|  | familySummary = sb.toString(); | 
|  | } | 
|  |  | 
|  | 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, | 
|  | familySummary)); | 
|  | 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 (Map.Entry<String, String> variable : variables.entrySet()) { | 
|  | line = line.replace("${" + variable.getKey() + "}", variable.getValue()); | 
|  | } | 
|  | return line; | 
|  | } | 
|  |  | 
|  | private static BufferedReader createReader(String filePath) throws IOException { | 
|  | File file = new File(filePath); | 
|  | if (file.exists()) { | 
|  | return Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8); | 
|  | } else { | 
|  | InputStream is = SourceFileReader.class.getResourceAsStream(filePath); | 
|  | if (is != null) { | 
|  | return new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); | 
|  | } else { | 
|  | return null; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | public static void readTextFile(String filePath, ReadAction action) | 
|  | throws BuildEncyclopediaDocException, IOException { | 
|  | try (BufferedReader br = createReader(filePath)) { | 
|  | 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); | 
|  | } | 
|  | } | 
|  | } | 
|  | } |