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 private 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