blob: bb26b1bd9a5ca564470484430229b7d0404f0e46 [file] [log] [blame]
// 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);
}
}
}
}