• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2016 Google Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 
15 package com.google.googlejavaformat.java.javadoc;
16 
17 import static com.google.common.base.Preconditions.checkNotNull;
18 import static com.google.common.collect.Comparators.max;
19 import static com.google.common.collect.Sets.immutableEnumSet;
20 import static com.google.googlejavaformat.java.javadoc.JavadocWriter.AutoIndent.AUTO_INDENT;
21 import static com.google.googlejavaformat.java.javadoc.JavadocWriter.AutoIndent.NO_AUTO_INDENT;
22 import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.BLANK_LINE;
23 import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NEWLINE;
24 import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.NONE;
25 import static com.google.googlejavaformat.java.javadoc.JavadocWriter.RequestedWhitespace.WHITESPACE;
26 import static com.google.googlejavaformat.java.javadoc.Token.Type.HEADER_OPEN_TAG;
27 import static com.google.googlejavaformat.java.javadoc.Token.Type.LIST_ITEM_OPEN_TAG;
28 import static com.google.googlejavaformat.java.javadoc.Token.Type.PARAGRAPH_OPEN_TAG;
29 
30 import com.google.common.collect.ImmutableSet;
31 import com.google.googlejavaformat.java.javadoc.Token.Type;
32 
33 /**
34  * Stateful object that accepts "requests" and "writes," producing formatted Javadoc.
35  *
36  * <p>Our Javadoc formatter doesn't ever generate a parse tree, only a stream of tokens, so the
37  * writer must compute and store the answer to questions like "How many levels of nested HTML list
38  * are we inside?"
39  */
40 final class JavadocWriter {
41   private final int blockIndent;
42   private final StringBuilder output = new StringBuilder();
43   /**
44    * Whether we are inside an {@code <li>} element, excluding the case in which the {@code <li>}
45    * contains a {@code <ul>} or {@code <ol>} that we are also inside -- unless of course we're
46    * inside an {@code <li>} element in that inner list :)
47    */
48   private boolean continuingListItemOfInnermostList;
49 
50   private boolean continuingFooterTag;
51   private final NestingCounter continuingListItemCount = new NestingCounter();
52   private final NestingCounter continuingListCount = new NestingCounter();
53   private final NestingCounter postWriteModifiedContinuingListCount = new NestingCounter();
54   private int remainingOnLine;
55   private boolean atStartOfLine;
56   private RequestedWhitespace requestedWhitespace = NONE;
57   private Token requestedMoeBeginStripComment;
58   private int indentForMoeEndStripComment;
59   private boolean wroteAnythingSignificant;
60 
JavadocWriter(int blockIndent)61   JavadocWriter(int blockIndent) {
62     this.blockIndent = blockIndent;
63   }
64 
65   /**
66    * Requests whitespace between the previously written token and the next written token. The
67    * request may be honored, or it may be overridden by a request for "more significant" whitespace,
68    * like a newline.
69    */
requestWhitespace()70   void requestWhitespace() {
71     requestWhitespace(WHITESPACE);
72   }
73 
requestMoeBeginStripComment(Token token)74   void requestMoeBeginStripComment(Token token) {
75     // We queue this up so that we can put it after any requested whitespace.
76     requestedMoeBeginStripComment = checkNotNull(token);
77   }
78 
writeBeginJavadoc()79   void writeBeginJavadoc() {
80     /*
81      * JavaCommentsHelper will make sure this is indented right. But it seems sensible enough that,
82      * if our input starts with ∕✱✱, so too does our output.
83      */
84     output.append("/**");
85     writeNewline();
86   }
87 
writeEndJavadoc()88   void writeEndJavadoc() {
89     output.append("\n");
90     appendSpaces(blockIndent + 1);
91     output.append("*/");
92   }
93 
writeFooterJavadocTagStart(Token token)94   void writeFooterJavadocTagStart(Token token) {
95     // Close any unclosed lists (e.g., <li> without <ul>).
96     // TODO(cpovirk): Actually generate </ul>, etc.?
97     /*
98      * TODO(cpovirk): Also generate </pre> and </table> if appropriate. This is necessary for
99      * idempotency in broken Javadoc. (We don't necessarily need that, but full idempotency may be a
100      * nice goal, especially if it helps us use a fuzzer to test.) Unfortunately, the writer doesn't
101      * currently know which of those tags are open.
102      */
103     continuingListItemOfInnermostList = false;
104     continuingListItemCount.reset();
105     continuingListCount.reset();
106     /*
107      * There's probably no need for this, since its only effect is to disable blank lines in some
108      * cases -- and we're doing that already in the footer.
109      */
110     postWriteModifiedContinuingListCount.reset();
111 
112     if (!wroteAnythingSignificant) {
113       // Javadoc consists solely of tags. This is frowned upon in general but OK for @Overrides.
114     } else if (!continuingFooterTag) {
115       // First footer tag after a body tag.
116       requestBlankLine();
117     } else {
118       // Subsequent footer tag.
119       continuingFooterTag = false;
120       requestNewline();
121     }
122     writeToken(token);
123     continuingFooterTag = true;
124   }
125 
writeListOpen(Token token)126   void writeListOpen(Token token) {
127     requestBlankLine();
128 
129     writeToken(token);
130     continuingListItemOfInnermostList = false;
131     continuingListCount.increment();
132     postWriteModifiedContinuingListCount.increment();
133 
134     requestNewline();
135   }
136 
writeListClose(Token token)137   void writeListClose(Token token) {
138     requestNewline();
139 
140     continuingListItemCount.decrementIfPositive();
141     continuingListCount.decrementIfPositive();
142     writeToken(token);
143     postWriteModifiedContinuingListCount.decrementIfPositive();
144 
145     requestBlankLine();
146   }
147 
writeListItemOpen(Token token)148   void writeListItemOpen(Token token) {
149     requestNewline();
150 
151     if (continuingListItemOfInnermostList) {
152       continuingListItemOfInnermostList = false;
153       continuingListItemCount.decrementIfPositive();
154     }
155     writeToken(token);
156     continuingListItemOfInnermostList = true;
157     continuingListItemCount.increment();
158   }
159 
writeHeaderOpen(Token token)160   void writeHeaderOpen(Token token) {
161     requestBlankLine();
162 
163     writeToken(token);
164   }
165 
writeHeaderClose(Token token)166   void writeHeaderClose(Token token) {
167     writeToken(token);
168 
169     requestBlankLine();
170   }
171 
writeParagraphOpen(Token token)172   void writeParagraphOpen(Token token) {
173     if (!wroteAnythingSignificant) {
174       /*
175        * The user included an initial <p> tag. Ignore it, and don't request a blank line before the
176        * next token.
177        */
178       return;
179     }
180 
181     requestBlankLine();
182 
183     writeToken(token);
184   }
185 
writeBlockquoteOpenOrClose(Token token)186   void writeBlockquoteOpenOrClose(Token token) {
187     requestBlankLine();
188 
189     writeToken(token);
190 
191     requestBlankLine();
192   }
193 
writePreOpen(Token token)194   void writePreOpen(Token token) {
195     requestBlankLine();
196 
197     writeToken(token);
198   }
199 
writePreClose(Token token)200   void writePreClose(Token token) {
201     writeToken(token);
202 
203     requestBlankLine();
204   }
205 
writeCodeOpen(Token token)206   void writeCodeOpen(Token token) {
207     writeToken(token);
208   }
209 
writeCodeClose(Token token)210   void writeCodeClose(Token token) {
211     writeToken(token);
212   }
213 
writeTableOpen(Token token)214   void writeTableOpen(Token token) {
215     requestBlankLine();
216 
217     writeToken(token);
218   }
219 
writeTableClose(Token token)220   void writeTableClose(Token token) {
221     writeToken(token);
222 
223     requestBlankLine();
224   }
225 
writeMoeEndStripComment(Token token)226   void writeMoeEndStripComment(Token token) {
227     writeLineBreakNoAutoIndent();
228     appendSpaces(indentForMoeEndStripComment);
229 
230     // Or maybe just "output.append(token.getValue())?" I'm kind of surprised this is so easy.
231     writeToken(token);
232 
233     requestNewline();
234   }
235 
writeHtmlComment(Token token)236   void writeHtmlComment(Token token) {
237     requestNewline();
238 
239     writeToken(token);
240 
241     requestNewline();
242   }
243 
writeBr(Token token)244   void writeBr(Token token) {
245     writeToken(token);
246 
247     requestNewline();
248   }
249 
writeLineBreakNoAutoIndent()250   void writeLineBreakNoAutoIndent() {
251     writeNewline(NO_AUTO_INDENT);
252   }
253 
writeLiteral(Token token)254   void writeLiteral(Token token) {
255     writeToken(token);
256   }
257 
258   @Override
toString()259   public String toString() {
260     return output.toString();
261   }
262 
requestBlankLine()263   private void requestBlankLine() {
264     requestWhitespace(BLANK_LINE);
265   }
266 
requestNewline()267   private void requestNewline() {
268     requestWhitespace(NEWLINE);
269   }
270 
requestWhitespace(RequestedWhitespace requestedWhitespace)271   private void requestWhitespace(RequestedWhitespace requestedWhitespace) {
272     this.requestedWhitespace = max(requestedWhitespace, this.requestedWhitespace);
273   }
274 
275   /**
276    * The kind of whitespace that has been requested between the previous and next tokens. The order
277    * of the values is significant: It goes from lowest priority to highest. For example, if the
278    * previous token requests {@link #BLANK_LINE} after it but the next token requests only {@link
279    * #NEWLINE} before it, we insert {@link #BLANK_LINE}.
280    */
281   enum RequestedWhitespace {
282     NONE,
283     WHITESPACE,
284     NEWLINE,
285     BLANK_LINE,
286     ;
287   }
288 
writeToken(Token token)289   private void writeToken(Token token) {
290     if (requestedMoeBeginStripComment != null) {
291       requestNewline();
292     }
293 
294     if (requestedWhitespace == BLANK_LINE
295         && (postWriteModifiedContinuingListCount.isPositive() || continuingFooterTag)) {
296       /*
297        * We don't write blank lines inside lists or footer tags, even in cases where we otherwise
298        * would (e.g., before a <p> tag). Justification: We don't write blank lines _between_ list
299        * items or footer tags, so it would be strange to write blank lines _within_ one. Of course,
300        * an alternative approach would be to go ahead and write blank lines between items/tags,
301        * either always or only in the case that an item contains a blank line.
302        */
303       requestedWhitespace = NEWLINE;
304     }
305 
306     if (requestedWhitespace == BLANK_LINE) {
307       writeBlankLine();
308       requestedWhitespace = NONE;
309     } else if (requestedWhitespace == NEWLINE) {
310       writeNewline();
311       requestedWhitespace = NONE;
312     }
313     boolean needWhitespace = (requestedWhitespace == WHITESPACE);
314 
315     /*
316      * Write a newline if necessary to respect the line limit. (But if we're at the beginning of the
317      * line, a newline won't help. Or it might help but only by separating "<p>veryverylongword,"
318      * which goes against our style.)
319      */
320     if (!atStartOfLine && token.length() + (needWhitespace ? 1 : 0) > remainingOnLine) {
321       writeNewline();
322     }
323     if (!atStartOfLine && needWhitespace) {
324       output.append(" ");
325       remainingOnLine--;
326     }
327 
328     if (requestedMoeBeginStripComment != null) {
329       output.append(requestedMoeBeginStripComment.getValue());
330       requestedMoeBeginStripComment = null;
331       indentForMoeEndStripComment = innerIndent();
332       requestNewline();
333       writeToken(token);
334       return;
335     }
336 
337     output.append(token.getValue());
338 
339     if (!START_OF_LINE_TOKENS.contains(token.getType())) {
340       atStartOfLine = false;
341     }
342 
343     /*
344      * TODO(cpovirk): We really want the number of "characters," not chars. Figure out what the
345      * right way of measuring that is (grapheme count (with BreakIterator?)? sum of widths of all
346      * graphemes? I don't think that our style guide is specific about this.). Moreover, I am
347      * probably brushing other problems with surrogates, etc. under the table. Hopefully I mostly
348      * get away with it by joining all non-space, non-tab characters together.
349      *
350      * Possibly the "width" question has no right answer:
351      * http://denisbider.blogspot.com/2015/09/when-monospace-fonts-arent-unicode.html
352      */
353     remainingOnLine -= token.length();
354     requestedWhitespace = NONE;
355     wroteAnythingSignificant = true;
356   }
357 
writeBlankLine()358   private void writeBlankLine() {
359     output.append("\n");
360     appendSpaces(blockIndent + 1);
361     output.append("*");
362     writeNewline();
363   }
364 
writeNewline()365   private void writeNewline() {
366     writeNewline(AUTO_INDENT);
367   }
368 
writeNewline(AutoIndent autoIndent)369   private void writeNewline(AutoIndent autoIndent) {
370     output.append("\n");
371     appendSpaces(blockIndent + 1);
372     output.append("*");
373     appendSpaces(1);
374     remainingOnLine = JavadocFormatter.MAX_LINE_LENGTH - blockIndent - 3;
375     if (autoIndent == AUTO_INDENT) {
376       appendSpaces(innerIndent());
377       remainingOnLine -= innerIndent();
378     }
379     atStartOfLine = true;
380   }
381 
382   enum AutoIndent {
383     AUTO_INDENT,
384     NO_AUTO_INDENT
385   }
386 
innerIndent()387   private int innerIndent() {
388     int innerIndent = continuingListItemCount.value() * 4 + continuingListCount.value() * 2;
389     if (continuingFooterTag) {
390       innerIndent += 4;
391     }
392     return innerIndent;
393   }
394 
395   // If this is a hotspot, keep a String of many spaces around, and call append(string, start, end).
appendSpaces(int count)396   private void appendSpaces(int count) {
397     output.append(" ".repeat(count));
398   }
399 
400   /**
401    * Tokens that are always pinned to the following token. For example, {@code <p>} in {@code <p>Foo
402    * bar} (never {@code <p> Foo bar} or {@code <p>\nFoo bar}).
403    *
404    * <p>This is not the only kind of "pinning" that we do: See also the joining of LITERAL tokens
405    * done by the lexer. The special pinning here is necessary because these tokens are not of type
406    * LITERAL (because they require other special handling).
407    */
408   private static final ImmutableSet<Type> START_OF_LINE_TOKENS =
409       immutableEnumSet(LIST_ITEM_OPEN_TAG, PARAGRAPH_OPEN_TAG, HEADER_OPEN_TAG);
410 }
411