• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2015 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;
16 
17 import com.google.common.base.MoreObjects;
18 import com.google.common.collect.ArrayListMultimap;
19 import com.google.common.collect.ImmutableList;
20 import com.google.common.collect.Iterables;
21 import com.google.common.collect.Multimap;
22 import com.google.googlejavaformat.Indent.Const;
23 import com.google.googlejavaformat.Input.Tok;
24 import com.google.googlejavaformat.Input.Token;
25 import com.google.googlejavaformat.Output.BreakTag;
26 import java.util.ArrayList;
27 import java.util.List;
28 import java.util.Optional;
29 
30 /**
31  * An {@code OpsBuilder} creates a list of {@link Op}s, which is turned into a {@link Doc} by {@link
32  * DocBuilder}.
33  */
34 public final class OpsBuilder {
35 
36   /** @return the actual size of the AST node at position, including comments. */
actualSize(int position, int length)37   public int actualSize(int position, int length) {
38     Token startToken = input.getPositionTokenMap().get(position);
39     int start = startToken.getTok().getPosition();
40     for (Tok tok : startToken.getToksBefore()) {
41       if (tok.isComment()) {
42         start = Math.min(start, tok.getPosition());
43       }
44     }
45     Token endToken = input.getPositionTokenMap().get(position + length - 1);
46     int end = endToken.getTok().getPosition() + endToken.getTok().length();
47     for (Tok tok : endToken.getToksAfter()) {
48       if (tok.isComment()) {
49         end = Math.max(end, tok.getPosition() + tok.length());
50       }
51     }
52     return end - start;
53   }
54 
55   /** @return the start column of the token at {@code position}, including leading comments. */
actualStartColumn(int position)56   public Integer actualStartColumn(int position) {
57     Token startToken = input.getPositionTokenMap().get(position);
58     int start = startToken.getTok().getPosition();
59     int line0 = input.getLineNumber(start);
60     for (Tok tok : startToken.getToksBefore()) {
61       if (line0 != input.getLineNumber(tok.getPosition())) {
62         return start;
63       }
64       if (tok.isComment()) {
65         start = Math.min(start, tok.getPosition());
66       }
67     }
68     return start;
69   }
70 
71   /** A request to add or remove a blank line in the output. */
72   public abstract static class BlankLineWanted {
73 
74     /** Always emit a blank line. */
75     public static final BlankLineWanted YES = new SimpleBlankLine(Optional.of(true));
76 
77     /** Never emit a blank line. */
78     public static final BlankLineWanted NO = new SimpleBlankLine(Optional.of(false));
79 
80     /**
81      * Explicitly preserve blank lines from the input (e.g. before the first member in a class
82      * declaration). Overrides conditional blank lines.
83      */
84     public static final BlankLineWanted PRESERVE =
85         new SimpleBlankLine(/* wanted= */ Optional.empty());
86 
87     /** Is the blank line wanted? */
wanted()88     public abstract Optional<Boolean> wanted();
89 
90     /** Merge this blank line request with another. */
merge(BlankLineWanted wanted)91     public abstract BlankLineWanted merge(BlankLineWanted wanted);
92 
93     /** Emit a blank line if the given break is taken. */
conditional(BreakTag breakTag)94     public static BlankLineWanted conditional(BreakTag breakTag) {
95       return new ConditionalBlankLine(ImmutableList.of(breakTag));
96     }
97 
98     private static final class SimpleBlankLine extends BlankLineWanted {
99       private final Optional<Boolean> wanted;
100 
SimpleBlankLine(Optional<Boolean> wanted)101       SimpleBlankLine(Optional<Boolean> wanted) {
102         this.wanted = wanted;
103       }
104 
105       @Override
wanted()106       public Optional<Boolean> wanted() {
107         return wanted;
108       }
109 
110       @Override
merge(BlankLineWanted other)111       public BlankLineWanted merge(BlankLineWanted other) {
112         return this;
113       }
114     }
115 
116     private static final class ConditionalBlankLine extends BlankLineWanted {
117 
118       private final ImmutableList<BreakTag> tags;
119 
ConditionalBlankLine(Iterable<BreakTag> tags)120       ConditionalBlankLine(Iterable<BreakTag> tags) {
121         this.tags = ImmutableList.copyOf(tags);
122       }
123 
124       @Override
wanted()125       public Optional<Boolean> wanted() {
126         for (BreakTag tag : tags) {
127           if (tag.wasBreakTaken()) {
128             return Optional.of(true);
129           }
130         }
131         return Optional.empty();
132       }
133 
134       @Override
merge(BlankLineWanted other)135       public BlankLineWanted merge(BlankLineWanted other) {
136         if (!(other instanceof ConditionalBlankLine)) {
137           return other;
138         }
139         return new ConditionalBlankLine(
140             Iterables.concat(this.tags, ((ConditionalBlankLine) other).tags));
141       }
142     }
143   }
144 
145   private final Input input;
146   private final List<Op> ops = new ArrayList<>();
147   private final Output output;
148   private static final Indent.Const ZERO = Indent.Const.ZERO;
149 
150   private int tokenI = 0;
151   private int inputPosition = Integer.MIN_VALUE;
152 
153   /** The number of unclosed open ops in the input stream. */
154   int depth = 0;
155 
156   /** Add an {@link Op}, and record open/close ops for later validation of unclosed levels. */
add(Op op)157   private void add(Op op) {
158     if (op instanceof OpenOp) {
159       depth++;
160     } else if (op instanceof CloseOp) {
161       depth--;
162       if (depth < 0) {
163         throw new AssertionError();
164       }
165     }
166     ops.add(op);
167   }
168 
169   /** Add a list of {@link Op}s. */
addAll(List<Op> ops)170   public final void addAll(List<Op> ops) {
171     for (Op op : ops) {
172       add(op);
173     }
174   }
175 
176   /**
177    * The {@code OpsBuilder} constructor.
178    *
179    * @param input the {@link Input}, used for retrieve information from the AST
180    * @param output the {@link Output}, used here only to record blank-line information
181    */
OpsBuilder(Input input, Output output)182   public OpsBuilder(Input input, Output output) {
183     this.input = input;
184     this.output = output;
185   }
186 
187   /** Get the {@code OpsBuilder}'s {@link Input}. */
getInput()188   public final Input getInput() {
189     return input;
190   }
191 
192   /** Returns the number of unclosed open ops in the input stream. */
depth()193   public int depth() {
194     return depth;
195   }
196 
197   /**
198    * Checks that all open ops in the op stream have matching close ops.
199    *
200    * @throws FormattingError if any ops were unclosed
201    */
checkClosed(int previous)202   public void checkClosed(int previous) {
203     if (depth != previous) {
204       throw new FormattingError(diagnostic(String.format("saw %d unclosed ops", depth)));
205     }
206   }
207 
208   /** Create a {@link FormatterDiagnostic} at the current position. */
diagnostic(String message)209   public FormatterDiagnostic diagnostic(String message) {
210     return input.createDiagnostic(inputPosition, message);
211   }
212 
213   /**
214    * Sync to position in the input. If we've skipped outputting any tokens that were present in the
215    * input tokens, output them here and optionally complain.
216    *
217    * @param inputPosition the {@code 0}-based input position
218    */
sync(int inputPosition)219   public final void sync(int inputPosition) {
220     if (inputPosition > this.inputPosition) {
221       ImmutableList<? extends Input.Token> tokens = input.getTokens();
222       int tokensN = tokens.size();
223       this.inputPosition = inputPosition;
224       if (tokenI < tokensN && inputPosition > tokens.get(tokenI).getTok().getPosition()) {
225         // Found a missing input token. Insert it and mark it missing (usually not good).
226         Input.Token token = tokens.get(tokenI++);
227         throw new FormattingError(
228             diagnostic(String.format("did not generate token \"%s\"", token.getTok().getText())));
229       }
230     }
231   }
232 
233   /** Output any remaining tokens from the input stream (e.g. terminal whitespace). */
drain()234   public final void drain() {
235     int inputPosition = input.getText().length() + 1;
236     if (inputPosition > this.inputPosition) {
237       ImmutableList<? extends Input.Token> tokens = input.getTokens();
238       int tokensN = tokens.size();
239       while (tokenI < tokensN && inputPosition > tokens.get(tokenI).getTok().getPosition()) {
240         Input.Token token = tokens.get(tokenI++);
241         add(
242             Doc.Token.make(
243                 token,
244                 Doc.Token.RealOrImaginary.IMAGINARY,
245                 ZERO,
246                 /* breakAndIndentTrailingComment= */ Optional.empty()));
247       }
248     }
249     this.inputPosition = inputPosition;
250     checkClosed(0);
251   }
252 
253   /**
254    * Open a new level by emitting an {@link OpenOp}.
255    *
256    * @param plusIndent the extra indent for the new level
257    */
open(Indent plusIndent)258   public final void open(Indent plusIndent) {
259     add(OpenOp.make(plusIndent));
260   }
261 
262   /** Close the current level, by emitting a {@link CloseOp}. */
close()263   public final void close() {
264     add(CloseOp.make());
265   }
266 
267   /** Return the text of the next {@link Input.Token}, or absent if there is none. */
peekToken()268   public final Optional<String> peekToken() {
269     return peekToken(0);
270   }
271 
272   /** Return the text of an upcoming {@link Input.Token}, or absent if there is none. */
peekToken(int skip)273   public final Optional<String> peekToken(int skip) {
274     ImmutableList<? extends Input.Token> tokens = input.getTokens();
275     int idx = tokenI + skip;
276     return idx < tokens.size()
277         ? Optional.of(tokens.get(idx).getTok().getOriginalText())
278         : Optional.empty();
279   }
280 
281   /**
282    * Emit an optional token iff it exists on the input. This is used to emit tokens whose existence
283    * has been lost in the AST.
284    *
285    * @param token the optional token
286    */
guessToken(String token)287   public final void guessToken(String token) {
288     token(
289         token,
290         Doc.Token.RealOrImaginary.IMAGINARY,
291         ZERO,
292         /* breakAndIndentTrailingComment=  */ Optional.empty());
293   }
294 
token( String token, Doc.Token.RealOrImaginary realOrImaginary, Indent plusIndentCommentsBefore, Optional<Indent> breakAndIndentTrailingComment)295   public final void token(
296       String token,
297       Doc.Token.RealOrImaginary realOrImaginary,
298       Indent plusIndentCommentsBefore,
299       Optional<Indent> breakAndIndentTrailingComment) {
300     ImmutableList<? extends Input.Token> tokens = input.getTokens();
301     if (token.equals(peekToken().orElse(null))) { // Found the input token. Output it.
302       add(
303           Doc.Token.make(
304               tokens.get(tokenI++),
305               Doc.Token.RealOrImaginary.REAL,
306               plusIndentCommentsBefore,
307               breakAndIndentTrailingComment));
308     } else {
309       /*
310        * Generated a "bad" token, which doesn't exist on the input. Drop it, and complain unless
311        * (for example) we're guessing at an optional token.
312        */
313       if (realOrImaginary.isReal()) {
314         throw new FormattingError(
315             diagnostic(
316                 String.format(
317                     "expected token: '%s'; generated %s instead",
318                     peekToken().orElse(null), token)));
319       }
320     }
321   }
322 
323   /**
324    * Emit a single- or multi-character op by breaking it into single-character {@link Doc.Token}s.
325    *
326    * @param op the operator to emit
327    */
op(String op)328   public final void op(String op) {
329     int opN = op.length();
330     for (int i = 0; i < opN; i++) {
331       token(
332           op.substring(i, i + 1),
333           Doc.Token.RealOrImaginary.REAL,
334           ZERO,
335           /* breakAndIndentTrailingComment=  */ Optional.empty());
336     }
337   }
338 
339   /** Emit a {@link Doc.Space}. */
space()340   public final void space() {
341     add(Doc.Space.make());
342   }
343 
344   /** Emit a {@link Doc.Break}. */
breakOp()345   public final void breakOp() {
346     breakOp(Doc.FillMode.UNIFIED, "", ZERO);
347   }
348 
349   /**
350    * Emit a {@link Doc.Break}.
351    *
352    * @param plusIndent extra indent if taken
353    */
breakOp(Indent plusIndent)354   public final void breakOp(Indent plusIndent) {
355     breakOp(Doc.FillMode.UNIFIED, "", plusIndent);
356   }
357 
358   /** Emit a filled {@link Doc.Break}. */
breakToFill()359   public final void breakToFill() {
360     breakOp(Doc.FillMode.INDEPENDENT, "", ZERO);
361   }
362 
363   /** Emit a forced {@link Doc.Break}. */
forcedBreak()364   public final void forcedBreak() {
365     breakOp(Doc.FillMode.FORCED, "", ZERO);
366   }
367 
368   /**
369    * Emit a forced {@link Doc.Break}.
370    *
371    * @param plusIndent extra indent if taken
372    */
forcedBreak(Indent plusIndent)373   public final void forcedBreak(Indent plusIndent) {
374     breakOp(Doc.FillMode.FORCED, "", plusIndent);
375   }
376 
377   /**
378    * Emit a {@link Doc.Break}, with a specified {@code flat} value (e.g., {@code " "}).
379    *
380    * @param flat the {@link Doc.Break} when not broken
381    */
breakOp(String flat)382   public final void breakOp(String flat) {
383     breakOp(Doc.FillMode.UNIFIED, flat, ZERO);
384   }
385 
386   /**
387    * Emit a {@link Doc.Break}, with a specified {@code flat} value (e.g., {@code " "}).
388    *
389    * @param flat the {@link Doc.Break} when not broken
390    */
breakToFill(String flat)391   public final void breakToFill(String flat) {
392     breakOp(Doc.FillMode.INDEPENDENT, flat, ZERO);
393   }
394 
395   /**
396    * Emit a generic {@link Doc.Break}.
397    *
398    * @param fillMode the {@link Doc.FillMode}
399    * @param flat the {@link Doc.Break} when not broken
400    * @param plusIndent extra indent if taken
401    */
breakOp(Doc.FillMode fillMode, String flat, Indent plusIndent)402   public final void breakOp(Doc.FillMode fillMode, String flat, Indent plusIndent) {
403     breakOp(fillMode, flat, plusIndent, /* optionalTag=  */ Optional.empty());
404   }
405 
406   /**
407    * Emit a generic {@link Doc.Break}.
408    *
409    * @param fillMode the {@link Doc.FillMode}
410    * @param flat the {@link Doc.Break} when not broken
411    * @param plusIndent extra indent if taken
412    * @param optionalTag an optional tag for remembering whether the break was taken
413    */
breakOp( Doc.FillMode fillMode, String flat, Indent plusIndent, Optional<BreakTag> optionalTag)414   public final void breakOp(
415       Doc.FillMode fillMode, String flat, Indent plusIndent, Optional<BreakTag> optionalTag) {
416     add(Doc.Break.make(fillMode, flat, plusIndent, optionalTag));
417   }
418 
419   private int lastPartialFormatBoundary = -1;
420 
421   /**
422    * Make the boundary of a region that can be partially formatted. The boundary will be included in
423    * the following region, e.g.: [[boundary0, boundary1), [boundary1, boundary2), ...].
424    */
markForPartialFormat()425   public void markForPartialFormat() {
426     if (lastPartialFormatBoundary == -1) {
427       lastPartialFormatBoundary = tokenI;
428       return;
429     }
430     if (tokenI == lastPartialFormatBoundary) {
431       return;
432     }
433     Token start = input.getTokens().get(lastPartialFormatBoundary);
434     Token end = input.getTokens().get(tokenI - 1);
435     output.markForPartialFormat(start, end);
436     lastPartialFormatBoundary = tokenI;
437   }
438 
439   /**
440    * Force or suppress a blank line here in the output.
441    *
442    * @param wanted whether to force ({@code true}) or suppress {@code false}) the blank line
443    */
blankLineWanted(BlankLineWanted wanted)444   public final void blankLineWanted(BlankLineWanted wanted) {
445     output.blankLine(getI(input.getTokens().get(tokenI)), wanted);
446   }
447 
getI(Input.Token token)448   private static int getI(Input.Token token) {
449     for (Input.Tok tok : token.getToksBefore()) {
450       if (tok.getIndex() >= 0) {
451         return tok.getIndex();
452       }
453     }
454     return token.getTok().getIndex();
455   }
456 
457   private static final Doc.Space SPACE = Doc.Space.make();
458 
459   /**
460    * Build a list of {@link Op}s from the {@code OpsBuilder}.
461    *
462    * @return the list of {@link Op}s
463    */
build()464   public final ImmutableList<Op> build() {
465     markForPartialFormat();
466     // Rewrite the ops to insert comments.
467     Multimap<Integer, Op> tokOps = ArrayListMultimap.create();
468     int opsN = ops.size();
469     for (int i = 0; i < opsN; i++) {
470       Op op = ops.get(i);
471       if (op instanceof Doc.Token) {
472         /*
473          * Token ops can have associated non-tokens, including comments, which we need to insert.
474          * They can also cause line breaks, so we insert them before or after the current level,
475          * when possible.
476          */
477         Doc.Token tokenOp = (Doc.Token) op;
478         Input.Token token = tokenOp.getToken();
479         int j = i; // Where to insert toksBefore before.
480         while (0 < j && ops.get(j - 1) instanceof OpenOp) {
481           --j;
482         }
483         int k = i; // Where to insert toksAfter after.
484         while (k + 1 < opsN && ops.get(k + 1) instanceof CloseOp) {
485           ++k;
486         }
487         if (tokenOp.realOrImaginary().isReal()) {
488           /*
489            * Regular input token. Copy out toksBefore before token, and toksAfter after it. Insert
490            * this token's toksBefore at position j.
491            */
492           int newlines = 0; // Count of newlines in a row.
493           boolean space = false; // Do we need an extra space after a previous "/*" comment?
494           boolean lastWasComment = false; // Was the last thing we output a comment?
495           boolean allowBlankAfterLastComment = false;
496           for (Input.Tok tokBefore : token.getToksBefore()) {
497             if (tokBefore.isNewline()) {
498               newlines++;
499             } else if (tokBefore.isComment()) {
500               tokOps.put(
501                   j,
502                   Doc.Break.make(
503                       tokBefore.isSlashSlashComment() ? Doc.FillMode.FORCED : Doc.FillMode.UNIFIED,
504                       "",
505                       tokenOp.getPlusIndentCommentsBefore()));
506               tokOps.putAll(j, makeComment(tokBefore));
507               space = tokBefore.isSlashStarComment();
508               newlines = 0;
509               lastWasComment = true;
510               if (tokBefore.isJavadocComment()) {
511                 tokOps.put(j, Doc.Break.makeForced());
512               }
513               allowBlankAfterLastComment =
514                   tokBefore.isSlashSlashComment()
515                       || (tokBefore.isSlashStarComment() && !tokBefore.isJavadocComment());
516             }
517           }
518           if (allowBlankAfterLastComment && newlines > 1) {
519             // Force a line break after two newlines in a row following a line or block comment
520             output.blankLine(token.getTok().getIndex(), BlankLineWanted.YES);
521           }
522           if (lastWasComment && newlines > 0) {
523             tokOps.put(j, Doc.Break.makeForced());
524           } else if (space) {
525             tokOps.put(j, SPACE);
526           }
527           // Now we've seen the Token; output the toksAfter.
528           for (Input.Tok tokAfter : token.getToksAfter()) {
529             if (tokAfter.isComment()) {
530               boolean breakAfter =
531                   tokAfter.isJavadocComment()
532                       || (tokAfter.isSlashStarComment()
533                           && tokenOp.breakAndIndentTrailingComment().isPresent());
534               if (breakAfter) {
535                 tokOps.put(
536                     k + 1,
537                     Doc.Break.make(
538                         Doc.FillMode.FORCED,
539                         "",
540                         tokenOp.breakAndIndentTrailingComment().orElse(Const.ZERO)));
541               } else {
542                 tokOps.put(k + 1, SPACE);
543               }
544               tokOps.putAll(k + 1, makeComment(tokAfter));
545               if (breakAfter) {
546                 tokOps.put(k + 1, Doc.Break.make(Doc.FillMode.FORCED, "", ZERO));
547               }
548             }
549           }
550         } else {
551           /*
552            * This input token was mistakenly not generated for output. As no whitespace or comments
553            * were generated (presumably), copy all input non-tokens literally, even spaces and
554            * newlines.
555            */
556           int newlines = 0;
557           boolean lastWasComment = false;
558           for (Input.Tok tokBefore : token.getToksBefore()) {
559             if (tokBefore.isNewline()) {
560               newlines++;
561             } else if (tokBefore.isComment()) {
562               newlines = 0;
563               lastWasComment = tokBefore.isComment();
564             }
565             if (lastWasComment && newlines > 0) {
566               tokOps.put(j, Doc.Break.makeForced());
567             }
568             tokOps.put(j, Doc.Tok.make(tokBefore));
569           }
570           for (Input.Tok tokAfter : token.getToksAfter()) {
571             tokOps.put(k + 1, Doc.Tok.make(tokAfter));
572           }
573         }
574       }
575     }
576     /*
577      * Construct new list of ops, splicing in the comments. If a comment is inserted immediately
578      * before a space, suppress the space.
579      */
580     ImmutableList.Builder<Op> newOps = ImmutableList.builder();
581     boolean afterForcedBreak = false; // Was the last Op a forced break? If so, suppress spaces.
582     for (int i = 0; i < opsN; i++) {
583       for (Op op : tokOps.get(i)) {
584         if (!(afterForcedBreak && op instanceof Doc.Space)) {
585           newOps.add(op);
586           afterForcedBreak = isForcedBreak(op);
587         }
588       }
589       Op op = ops.get(i);
590       if (afterForcedBreak
591           && (op instanceof Doc.Space
592               || (op instanceof Doc.Break
593                   && ((Doc.Break) op).getPlusIndent() == 0
594                   && " ".equals(((Doc) op).getFlat())))) {
595         continue;
596       }
597       newOps.add(op);
598       if (!(op instanceof OpenOp)) {
599         afterForcedBreak = isForcedBreak(op);
600       }
601     }
602     for (Op op : tokOps.get(opsN)) {
603       if (!(afterForcedBreak && op instanceof Doc.Space)) {
604         newOps.add(op);
605         afterForcedBreak = isForcedBreak(op);
606       }
607     }
608     return newOps.build();
609   }
610 
isForcedBreak(Op op)611   private static boolean isForcedBreak(Op op) {
612     return op instanceof Doc.Break && ((Doc.Break) op).isForced();
613   }
614 
makeComment(Input.Tok comment)615   private static List<Op> makeComment(Input.Tok comment) {
616     return comment.isSlashStarComment()
617         ? ImmutableList.of(Doc.Tok.make(comment))
618         : ImmutableList.of(Doc.Tok.make(comment), Doc.Break.makeForced());
619   }
620 
621   @Override
toString()622   public final String toString() {
623     return MoreObjects.toStringHelper(this)
624         .add("input", input)
625         .add("ops", ops)
626         .add("output", output)
627         .add("tokenI", tokenI)
628         .add("inputPosition", inputPosition)
629         .toString();
630   }
631 }
632