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