aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/tools/skylark/java/com/google/devtools/skylark/skylint/DocstringUtils.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/tools/skylark/java/com/google/devtools/skylark/skylint/DocstringUtils.java')
-rw-r--r--src/tools/skylark/java/com/google/devtools/skylark/skylint/DocstringUtils.java258
1 files changed, 258 insertions, 0 deletions
diff --git a/src/tools/skylark/java/com/google/devtools/skylark/skylint/DocstringUtils.java b/src/tools/skylark/java/com/google/devtools/skylark/skylint/DocstringUtils.java
new file mode 100644
index 0000000000..747aec2ca8
--- /dev/null
+++ b/src/tools/skylark/java/com/google/devtools/skylark/skylint/DocstringUtils.java
@@ -0,0 +1,258 @@
+// 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
+ * parameter2: description of the second
+ * parameter that spans two lines. Each additional line
+ * must be indented by (at least) two spaces
+ * another_parameter (unused, mutable): a parameter may be followed
+ * by additional attributes in parentheses
+ * """
+ * }</pre>
+ *
+ * @param docstring a docstring of the format described above
+ * @param parseErrors a list to which parsing error messages are written
+ * @return the parsed docstring information
+ */
+ static DocstringInfo parseDocString(String docstring, List<DocstringParseError> parseErrors) {
+ DocstringParser parser = new DocstringParser(docstring);
+ DocstringInfo result = parser.parse();
+ parseErrors.addAll(parser.errors);
+ return result;
+ }
+
+ static class DocstringInfo {
+ final String summary;
+ final List<ParameterDoc> parameters;
+ final String longDescription;
+
+ public DocstringInfo(String summary, List<ParameterDoc> parameters, String longDescription) {
+ this.summary = summary;
+ this.parameters = ImmutableList.copyOf(parameters);
+ this.longDescription = longDescription;
+ }
+ }
+
+ 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 expectedIndentation = 0;
+ private String line = "";
+ private final List<DocstringParseError> errors = new ArrayList<>();
+
+ DocstringParser(String docstring) {
+ this.docstring = docstring;
+ nextLine();
+ }
+
+ boolean nextLine() {
+ if (startOfLineOffset >= docstring.length()) {
+ return false;
+ }
+ 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 < expectedIndentation) {
+ error(
+ "line indented too little (here: "
+ + indentation
+ + " spaces; before: "
+ + expectedIndentation
+ + " spaces)");
+ expectedIndentation = indentation;
+ }
+ startOfLineOffset += expectedIndentation;
+ }
+ line = docstring.substring(startOfLineOffset, endOfLineOffset);
+ return true;
+ }
+
+ 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();
+ }
+ expectedIndentation = getIndentation(line);
+ line = line.substring(expectedIndentation);
+ List<String> longDescriptionLines = new ArrayList<>();
+ List<ParameterDoc> params = new ArrayList<>();
+ boolean sectionStart = true;
+ do {
+ if (line.startsWith(" ")) {
+ error(
+ "line indented too much (here: "
+ + (expectedIndentation + getIndentation(line))
+ + " spaces; expected: "
+ + expectedIndentation
+ + " spaces)");
+ }
+ if (line.equals("Args:")) {
+ if (!sectionStart) {
+ error("section should be preceded by a blank line");
+ }
+ if (!params.isEmpty()) {
+ error("parameters were already documented before");
+ }
+ params.addAll(parseParameters());
+ } else {
+ longDescriptionLines.add(line);
+ }
+ sectionStart = line.isEmpty();
+ } while (nextLine());
+ return new DocstringInfo(summary, params, 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<>();
+ while (!line.isEmpty()) {
+ if (getIndentation(line) != 2) {
+ error("parameter lines have to be indented by two spaces");
+ } else {
+ line = line.substring(2);
+ }
+ Matcher matcher = paramLineMatcher.matcher(line);
+ 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));
+ while (nextLine() && getIndentation(line) > 2) {
+ description.append('\n');
+ description.append(line, getIndentation(line), line.length());
+ }
+ params.add(new ParameterDoc(parameterName, attributes, description.toString()));
+ }
+ return params;
+ }
+ }
+
+ 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;
+ }
+ }
+}