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