blob: 5d0ec5218dd242d7a4f9e2e642beb7c99eaf9158 [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
*
* https://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 org.apache.avro.compiler.idl;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility class with {@code ThreadLocal} fields that allow the generated
* classes {@link Idl} and {@link IdlTokenManager} to exchange documentation
* comments without forcing explicit parsing of documentation comments.
*
* The reason this works is that all calls to this class happen within a call to
* the method {@link Idl#CompilationUnit()} (either directly or indirectly).
*/
public class DocCommentHelper {
/**
* Pattern to match the common whitespace indents in a multi-line String.
* Doesn't match a single-line String, fully matches any multi-line String.
*
* To use: match on a {@link String#trim() trimmed} String, and then replace all
* newlines followed by the group "indent" with a newline.
*/
private static final Pattern WS_INDENT = Pattern.compile("(?U).*\\R(?<indent>\\h*).*(?:\\R\\k<indent>.*)*");
/**
* Pattern to match the whitespace indents plus common stars (1 or 2) in a
* multi-line String. If a String fully matches, replace all occurrences of a
* newline followed by whitespace and then the group "stars" with a newline.
*
* Note: partial matches are invalid.
*/
private static final Pattern STAR_INDENT = Pattern.compile("(?U)(?<stars>\\*{1,2}).*(?:\\R\\h*\\k<stars>.*)*");
private static final ThreadLocal<DocComment> DOC = new ThreadLocal<>();
private static final ThreadLocal<List<String>> WARNINGS = ThreadLocal.withInitial(ArrayList::new);
/**
* Return all warnings that were encountered while parsing, once. Subsequent
* calls before parsing again will return an empty list.
*/
static List<String> getAndClearWarnings() {
List<String> warnings = WARNINGS.get();
WARNINGS.remove();
return warnings;
}
static void setDoc(Token token) {
DocComment newDocComment = new DocComment(token);
DocComment oldDocComment = DOC.get();
if (oldDocComment != null) {
WARNINGS.get()
.add(String.format(
"Found documentation comment at line %d, column %d. Ignoring previous one at line %d, column %d: \"%s\"\n"
+ "Did you mean to use a multiline comment ( /* ... */ ) instead?",
newDocComment.line, newDocComment.column, oldDocComment.line, oldDocComment.column, oldDocComment.text));
}
DOC.set(newDocComment);
}
/**
* Clear any documentation (and generate a warning if there was).
*
* This method should NOT be used after an optional component in a grammar
* (i.e., after a @code{[…]} or @code{…*} construct), because the optional
* grammar part may have already caused parsing a doc comment special token
* placed after the code block.
*/
static void clearDoc() {
DocComment oldDocComment = DOC.get();
if (oldDocComment != null) {
WARNINGS.get()
.add(String.format(
"Ignoring out-of-place documentation comment at line %d, column %d: \"%s\"\n"
+ "Did you mean to use a multiline comment ( /* ... */ ) instead?",
oldDocComment.line, oldDocComment.column, oldDocComment.text));
}
DOC.remove();
}
static String getDoc() {
DocComment docComment = DOC.get();
DOC.remove();
return docComment == null ? null : docComment.text;
}
/* Package private to facilitate testing */
static String stripIndents(String doc) {
Matcher starMatcher = STAR_INDENT.matcher(doc);
if (starMatcher.matches()) {
return doc.replaceAll("(?U)(?:^|(\\R)\\h*)\\Q" + starMatcher.group("stars") + "\\E\\h?", "$1");
}
Matcher whitespaceMatcher = WS_INDENT.matcher(doc);
if (whitespaceMatcher.matches()) {
return doc.replaceAll("(?U)(\\R)" + whitespaceMatcher.group("indent"), "$1");
}
return doc;
}
private static class DocComment {
private final String text;
private final int line;
private final int column;
DocComment(Token token) {
// The token is everything after the initial '/**', including all
// whitespace and the ending '*/'
int tokenLength = token.image.length();
this.text = stripIndents(token.image.substring(0, tokenLength - 2).trim());
this.line = token.beginLine;
// The preceding token was "/**", and the current token includes
// everything since (also all whitespace). Thus, we can safely subtract 3
// from the token column to get the start of the doc comment.
this.column = token.beginColumn - 3;
}
}
}