com.palantir.javaformat.java.javadoc.JavadocWriter Maven / Gradle / Ivy
Show all versions of palantir-java-format Show documentation
/*
* Copyright 2016 Google Inc.
*
* 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.palantir.javaformat.java.javadoc;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Sets.immutableEnumSet;
import static com.palantir.javaformat.java.javadoc.JavadocWriter.AutoIndent.AUTO_INDENT;
import static com.palantir.javaformat.java.javadoc.JavadocWriter.AutoIndent.NO_AUTO_INDENT;
import static com.palantir.javaformat.java.javadoc.JavadocWriter.RequestedWhitespace.BLANK_LINE;
import static com.palantir.javaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NEWLINE;
import static com.palantir.javaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NONE;
import static com.palantir.javaformat.java.javadoc.JavadocWriter.RequestedWhitespace.WHITESPACE;
import static com.palantir.javaformat.java.javadoc.Token.Type.HEADER_OPEN_TAG;
import static com.palantir.javaformat.java.javadoc.Token.Type.LIST_ITEM_OPEN_TAG;
import static com.palantir.javaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Ordering;
/**
* Stateful object that accepts "requests" and "writes," producing formatted Javadoc.
*
* Our Javadoc formatter doesn't ever generate a parse tree, only a stream of tokens, so the writer must compute and
* store the answer to questions like "How many levels of nested HTML list are we inside?"
*/
final class JavadocWriter {
private final int blockIndent;
private final int maxLineLength;
private final StringBuilder output = new StringBuilder();
/**
* Whether we are inside an {@code
} element, excluding the case in which the {@code } contains a
* {@code } or {@code } that we are also inside -- unless of course we're inside an {@code - } element in
* that inner list :)
*/
private boolean continuingListItemOfInnermostList;
private boolean continuingFooterTag;
private final NestingCounter continuingListItemCount = new NestingCounter();
private final NestingCounter continuingListCount = new NestingCounter();
private final NestingCounter postWriteModifiedContinuingListCount = new NestingCounter();
private int remainingOnLine;
private boolean atStartOfLine;
private RequestedWhitespace requestedWhitespace = NONE;
private Token requestedMoeBeginStripComment;
private int indentForMoeEndStripComment;
private boolean wroteAnythingSignificant;
JavadocWriter(int blockIndent, int maxLineLength) {
this.blockIndent = blockIndent;
this.maxLineLength = maxLineLength;
}
/**
* Requests whitespace between the previously written token and the next written token. The request may be honored,
* or it may be overridden by a request for "more significant" whitespace, like a newline.
*/
void requestWhitespace() {
requestWhitespace(WHITESPACE);
}
void requestMoeBeginStripComment(Token token) {
// We queue this up so that we can put it after any requested whitespace.
requestedMoeBeginStripComment = checkNotNull(token);
}
void writeBeginJavadoc() {
/*
* JavaCommentsHelper will make sure this is indented right. But it seems sensible enough that,
* if our input starts with ∕✱✱, so too does our output.
*/
output.append("/**");
writeNewline();
}
void writeEndJavadoc() {
output.append("\n");
appendSpaces(blockIndent + 1);
output.append("*/");
}
void writeFooterJavadocTagStart(Token token) {
// Close any unclosed lists (e.g.,
- without
).
// TODO(cpovirk): Actually generate
, etc.?
/*
* TODO(cpovirk): Also generate
and if appropriate. This is necessary for
* idempotency in broken Javadoc. (We don't necessarily need that, but full idempotency may be a
* nice goal, especially if it helps us use a fuzzer to test.) Unfortunately, the writer doesn't
* currently know which of those tags are open.
*/
continuingListItemOfInnermostList = false;
continuingListItemCount.reset();
continuingListCount.reset();
/*
* There's probably no need for this, since its only effect is to disable blank lines in some
* cases -- and we're doing that already in the footer.
*/
postWriteModifiedContinuingListCount.reset();
if (!wroteAnythingSignificant) {
// Javadoc consists solely of tags. This is frowned upon in general but OK for @Overrides.
} else if (!continuingFooterTag) {
// First footer tag after a body tag.
requestBlankLine();
} else {
// Subsequent footer tag.
continuingFooterTag = false;
requestNewline();
}
writeToken(token);
continuingFooterTag = true;
}
void writeListOpen(Token token) {
requestBlankLine();
writeToken(token);
continuingListItemOfInnermostList = false;
continuingListCount.increment();
postWriteModifiedContinuingListCount.increment();
requestNewline();
}
void writeListClose(Token token) {
requestNewline();
continuingListItemCount.decrementIfPositive();
continuingListCount.decrementIfPositive();
writeToken(token);
postWriteModifiedContinuingListCount.decrementIfPositive();
requestBlankLine();
}
void writeListItemOpen(Token token) {
requestNewline();
if (continuingListItemOfInnermostList) {
continuingListItemOfInnermostList = false;
continuingListItemCount.decrementIfPositive();
}
writeToken(token);
continuingListItemOfInnermostList = true;
continuingListItemCount.increment();
}
void writeHeaderOpen(Token token) {
requestBlankLine();
writeToken(token);
}
void writeHeaderClose(Token token) {
writeToken(token);
requestBlankLine();
}
void writeParagraphOpen(Token token) {
if (!wroteAnythingSignificant) {
/*
* The user included an initial tag. Ignore it, and don't request a blank line before the * next token. */ return; } requestBlankLine(); writeToken(token); } void writeBlockquoteOpenOrClose(Token token) { requestBlankLine(); writeToken(token); requestBlankLine(); } void writePreOpen(Token token) { requestBlankLine(); writeToken(token); } void writePreClose(Token token) { writeToken(token); requestBlankLine(); } void writeCodeOpen(Token token) { writeToken(token); } void writeCodeClose(Token token) { writeToken(token); } void writeTableOpen(Token token) { requestBlankLine(); writeToken(token); } void writeTableClose(Token token) { writeToken(token); requestBlankLine(); } void writeMoeEndStripComment(Token token) { writeLineBreakNoAutoIndent(); appendSpaces(indentForMoeEndStripComment); // Or maybe just "output.append(token.getValue())?" I'm kind of surprised this is so easy. writeToken(token); requestNewline(); } void writeHtmlComment(Token token) { requestNewline(); writeToken(token); requestNewline(); } void writeBr(Token token) { writeToken(token); requestNewline(); } void writeLineBreakNoAutoIndent() { writeNewline(NO_AUTO_INDENT); } void writeLiteral(Token token) { writeToken(token); } @Override public String toString() { return output.toString(); } private void requestBlankLine() { requestWhitespace(BLANK_LINE); } private void requestNewline() { requestWhitespace(NEWLINE); } private void requestWhitespace(RequestedWhitespace requestedWhitespace) { this.requestedWhitespace = Ordering.natural().max(requestedWhitespace, this.requestedWhitespace); } /** * The kind of whitespace that has been requested between the previous and next tokens. The order of the values is * significant: It goes from lowest priority to highest. For example, if the previous token requests * {@link #BLANK_LINE} after it but the next token requests only {@link #NEWLINE} before it, we insert * {@link #BLANK_LINE}. */ enum RequestedWhitespace { NONE, WHITESPACE, NEWLINE, BLANK_LINE, ; } private void writeToken(Token token) { if (requestedMoeBeginStripComment != null) { requestNewline(); } if (requestedWhitespace == BLANK_LINE && (postWriteModifiedContinuingListCount.isPositive() || continuingFooterTag)) { /* * We don't write blank lines inside lists or footer tags, even in cases where we otherwise * would (e.g., before a
tag). Justification: We don't write blank lines _between_ list * items or footer tags, so it would be strange to write blank lines _within_ one. Of course, * an alternative approach would be to go ahead and write blank lines between items/tags, * either always or only in the case that an item contains a blank line. */ requestedWhitespace = NEWLINE; } if (requestedWhitespace == BLANK_LINE) { writeBlankLine(); requestedWhitespace = NONE; } else if (requestedWhitespace == NEWLINE) { writeNewline(); requestedWhitespace = NONE; } boolean needWhitespace = (requestedWhitespace == WHITESPACE); /* * Write a newline if necessary to respect the line limit. (But if we're at the beginning of the * line, a newline won't help. Or it might help but only by separating "
veryverylongword," * which goes against our style.) */ if (!atStartOfLine && token.length() + (needWhitespace ? 1 : 0) > remainingOnLine) { writeNewline(); } if (!atStartOfLine && needWhitespace) { output.append(" "); remainingOnLine--; } if (requestedMoeBeginStripComment != null) { output.append(requestedMoeBeginStripComment.getValue()); requestedMoeBeginStripComment = null; indentForMoeEndStripComment = innerIndent(); requestNewline(); writeToken(token); return; } output.append(token.getValue()); if (!START_OF_LINE_TOKENS.contains(token.getType())) { atStartOfLine = false; } /* * TODO(cpovirk): We really want the number of "characters," not chars. Figure out what the * right way of measuring that is (grapheme count (with BreakIterator?)? sum of widths of all * graphemes? I don't think that our style guide is specific about this.). Moreover, I am * probably brushing other problems with surrogates, etc. under the table. Hopefully I mostly * get away with it by joining all non-space, non-tab characters together. * * Possibly the "width" question has no right answer: * http://denisbider.blogspot.com/2015/09/when-monospace-fonts-arent-unicode.html */ remainingOnLine -= token.length(); requestedWhitespace = NONE; wroteAnythingSignificant = true; } private void writeBlankLine() { output.append("\n"); appendSpaces(blockIndent + 1); output.append("*"); writeNewline(); } private void writeNewline() { writeNewline(AUTO_INDENT); } private void writeNewline(AutoIndent autoIndent) { output.append("\n"); appendSpaces(blockIndent + 1); output.append("*"); appendSpaces(1); remainingOnLine = maxLineLength - blockIndent - 3; if (autoIndent == AUTO_INDENT) { appendSpaces(innerIndent()); remainingOnLine -= innerIndent(); } atStartOfLine = true; } enum AutoIndent { AUTO_INDENT, NO_AUTO_INDENT } private int innerIndent() { int innerIndent = continuingListItemCount.value() * 4 + continuingListCount.value() * 2; if (continuingFooterTag) { innerIndent += 4; } return innerIndent; } // If this is a hotspot, keep a String of many spaces around, and call append(string, start, end). private void appendSpaces(int count) { output.append(" ".repeat(count)); } /** * Tokens that are always pinned to the following token. For example, {@code
} in {@code
Foo bar} (never * {@code
Foo bar} or {@code
\nFoo bar}). * *
This is not the only kind of "pinning" that we do: See also the joining of LITERAL tokens done by the lexer.
* The special pinning here is necessary because these tokens are not of type LITERAL (because they require other
* special handling).
*/
private static final ImmutableSet