blob: 9cc0cacd9b7a81bf2e0d7310b5182abc80e6bd32 [file]
// Copyright 2017 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.skylark.skylint;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.lib.syntax.Expression;
import com.google.devtools.build.lib.syntax.ExpressionStatement;
import com.google.devtools.build.lib.syntax.Statement;
import com.google.devtools.build.lib.syntax.StringLiteral;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/** Utilities to extract and parse docstrings. */
public final class DocstringUtils {
private DocstringUtils() {}
@Nullable
static StringLiteral extractDocstring(List<Statement> statements) {
if (statements.isEmpty()) {
return null;
}
Statement statement = statements.get(0);
if (statement instanceof ExpressionStatement) {
Expression expr = ((ExpressionStatement) statement).getExpression();
if (expr instanceof StringLiteral) {
return (StringLiteral) expr;
}
}
return null;
}
/**
* Parses a docstring.
*
* <p>The format of the docstring is as follows
*
* <pre>{@code
* """One-line summary: must be followed and may be preceded by a blank line.
*
* Optional additional description like this.
*
* If it's a function docstring and the function has more than one argument, the docstring has
* to document these parameters as follows:
*
* Args:
* parameter1: description of the first parameter. Each parameter line
* should be indented by one, preferably two, spaces (as here).
* parameter2: description of the second
* parameter that spans two lines. Each additional line should have a
* hanging indentation of at least one, preferably two, additional spaces (as here).
* another_parameter (unused, mutable): a parameter may be followed
* by additional attributes in parentheses
*
* Returns:
* Description of the return value.
* Should be indented by at least one, preferably two spaces (as here)
* Can span multiple lines.
* """
* }</pre>
*
* @param docstring a docstring of the format described above
* @param indentation the indentation level (number of spaces) of the docstring
* @param parseErrors a list to which parsing error messages are written
* @return the parsed docstring information
*/
static DocstringInfo parseDocstring(
String docstring, int indentation, List<DocstringParseError> parseErrors) {
DocstringParser parser = new DocstringParser(docstring, indentation);
DocstringInfo result = parser.parse();
parseErrors.addAll(parser.errors);
return result;
}
static class DocstringInfo {
final String summary;
final List<ParameterDoc> parameters;
final String returns;
final String longDescription;
public DocstringInfo(
String summary, List<ParameterDoc> parameters, String returns, String longDescription) {
this.summary = summary;
this.parameters = ImmutableList.copyOf(parameters);
this.returns = returns;
this.longDescription = longDescription;
}
public boolean isSingleLineDocstring() {
return longDescription.isEmpty() && parameters.isEmpty() && returns.isEmpty();
}
}
static class ParameterDoc {
final String parameterName;
final List<String> attributes; // e.g. a type annotation, "unused", "mutable"
final String description;
public ParameterDoc(String parameterName, List<String> attributes, String description) {
this.parameterName = parameterName;
this.attributes = ImmutableList.copyOf(attributes);
this.description = description;
}
}
private static class DocstringParser {
private final String docstring;
private int startOfLineOffset = 0;
private int endOfLineOffset = -1;
private int lineNumber = 0;
private int baselineIndentation = 0;
private boolean blankLineBefore = false;
private String line = "";
private final List<DocstringParseError> errors = new ArrayList<>();
DocstringParser(String docstring, int indentation) {
this.docstring = docstring;
nextLine();
// the indentation is only relevant for the following lines, not the first one:
this.baselineIndentation = indentation;
}
boolean nextLine() {
if (startOfLineOffset >= docstring.length()) {
return false;
}
blankLineBefore = line.isEmpty();
lineNumber++;
startOfLineOffset = endOfLineOffset + 1;
if (startOfLineOffset >= docstring.length()) {
line = "";
return false;
}
endOfLineOffset = docstring.indexOf('\n', startOfLineOffset);
if (endOfLineOffset < 0) {
endOfLineOffset = docstring.length();
}
line = docstring.substring(startOfLineOffset, endOfLineOffset);
int indentation = getIndentation(line);
if (!line.isEmpty()) {
if (indentation < baselineIndentation) {
error(
"line indented too little (here: "
+ indentation
+ " spaces; expected: "
+ baselineIndentation
+ " spaces)");
startOfLineOffset += indentation;
} else {
startOfLineOffset += baselineIndentation;
}
}
line = docstring.substring(startOfLineOffset, endOfLineOffset);
return true;
}
private boolean eof() {
return startOfLineOffset >= docstring.length();
}
private static int getIndentation(String line) {
int index = 0;
while (index < line.length() && line.charAt(index) == ' ') {
index++;
}
return index;
}
void error(String message) {
errors.add(new DocstringParseError(message, lineNumber));
}
DocstringInfo parse() {
String summary = line;
if (!nextLine()) {
return new DocstringInfo(summary, Collections.emptyList(), "", "");
}
if (!line.isEmpty()) {
error("the one-line summary should be followed by a blank line");
} else {
nextLine();
}
List<String> longDescriptionLines = new ArrayList<>();
List<ParameterDoc> params = new ArrayList<>();
String returns = "";
while (!eof()) {
switch (line) {
case "Args:":
if (!blankLineBefore) {
error("section should be preceded by a blank line");
}
if (!params.isEmpty()) {
error("parameters were already documented before");
}
if (!returns.isEmpty()) {
error("parameters should be documented before the return value");
}
params.addAll(parseParameters());
break;
case "Returns:":
if (!blankLineBefore) {
error("section should be preceded by a blank line");
}
if (!returns.isEmpty()) {
error("return value was already documented before");
}
returns = parseSectionAfterHeading();
break;
default:
longDescriptionLines.add(line);
nextLine();
}
}
return new DocstringInfo(summary, params, returns, String.join("\n", longDescriptionLines));
}
private static final Pattern paramLineMatcher =
Pattern.compile(
"\\s*(?<name>[*\\w]+)( \\(\\s*(?<attributes>.*)\\s*\\))?: (?<description>.*)");
private static final Pattern attributesSeparator = Pattern.compile("\\s*,\\s*");
private List<ParameterDoc> parseParameters() {
nextLine();
List<ParameterDoc> params = new ArrayList<>();
int expectedParamLineIndentation = -1;
while (!eof()) {
if (line.isEmpty()) {
nextLine();
continue;
}
int actualIndentation = getIndentation(line);
if (actualIndentation == 0) {
if (!blankLineBefore) {
error("end of 'Args' section without blank line");
}
break;
}
String trimmedLine;
if (expectedParamLineIndentation == -1) {
expectedParamLineIndentation = actualIndentation;
}
if (expectedParamLineIndentation != actualIndentation) {
error(
"inconsistent indentation of parameter lines (before: "
+ expectedParamLineIndentation
+ "; here: "
+ actualIndentation
+ " spaces)");
}
trimmedLine = line.substring(actualIndentation);
Matcher matcher = paramLineMatcher.matcher(trimmedLine);
if (!matcher.matches()) {
error("invalid parameter documentation");
nextLine();
continue;
}
String parameterName = Preconditions.checkNotNull(matcher.group("name"));
String attributesString = matcher.group("attributes");
StringBuilder description = new StringBuilder(matcher.group("description"));
List<String> attributes =
attributesString == null
? Collections.emptyList()
: Arrays.asList(attributesSeparator.split(attributesString));
parseContinuedParamDescription(actualIndentation, description);
params.add(new ParameterDoc(parameterName, attributes, description.toString().trim()));
}
return params;
}
/** Parses additional lines that can come after "param: foo" in an 'Args' section. */
private void parseContinuedParamDescription(
int baselineIndentation, StringBuilder description) {
while (nextLine()) {
if (line.isEmpty()) {
description.append('\n');
continue;
}
if (getIndentation(line) <= baselineIndentation) {
break;
}
String trimmedLine = line.substring(baselineIndentation);
description.append('\n');
description.append(trimmedLine);
}
}
private String parseSectionAfterHeading() {
nextLine();
StringBuilder returns = new StringBuilder();
boolean firstLine = true;
while (!eof()) {
String trimmedLine;
if (line.isEmpty()) {
trimmedLine = line;
} else if (getIndentation(line) == 0) {
if (!blankLineBefore) {
error("end of section without blank line");
}
break;
} else {
if (getIndentation(line) < 2) {
error(
"text in a section has to be indented by two spaces"
+ " (relative to the left margin of the docstring)");
trimmedLine = line.substring(getIndentation(line));
} else {
trimmedLine = line.substring(2);
}
}
if (!firstLine) {
returns.append('\n');
}
returns.append(trimmedLine);
nextLine();
firstLine = false;
}
return returns.toString().trim();
}
}
static class DocstringParseError {
final String message;
final int lineNumber;
public DocstringParseError(String message, int lineNumber) {
this.message = message;
this.lineNumber = lineNumber;
}
@Override
public String toString() {
return ":" + lineNumber + ": " + message;
}
}
}