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.java; 16 17 import static com.google.common.base.Preconditions.checkNotNull; 18 import static com.google.common.collect.Iterables.getLast; 19 import static java.nio.charset.StandardCharsets.UTF_8; 20 21 import com.google.common.base.MoreObjects; 22 import com.google.common.base.Verify; 23 import com.google.common.collect.DiscreteDomain; 24 import com.google.common.collect.ImmutableCollection; 25 import com.google.common.collect.ImmutableList; 26 import com.google.common.collect.ImmutableMap; 27 import com.google.common.collect.ImmutableRangeMap; 28 import com.google.common.collect.ImmutableSet; 29 import com.google.common.collect.Iterators; 30 import com.google.common.collect.Range; 31 import com.google.common.collect.RangeSet; 32 import com.google.common.collect.TreeRangeSet; 33 import com.google.googlejavaformat.Input; 34 import com.google.googlejavaformat.Newlines; 35 import com.google.googlejavaformat.java.JavacTokens.RawTok; 36 import com.sun.tools.javac.file.JavacFileManager; 37 import com.sun.tools.javac.parser.Tokens.TokenKind; 38 import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; 39 import com.sun.tools.javac.util.Context; 40 import com.sun.tools.javac.util.Log; 41 import com.sun.tools.javac.util.Log.DeferredDiagnosticHandler; 42 import com.sun.tools.javac.util.Options; 43 import java.io.IOException; 44 import java.net.URI; 45 import java.util.ArrayList; 46 import java.util.Collection; 47 import java.util.Iterator; 48 import java.util.List; 49 import javax.tools.Diagnostic; 50 import javax.tools.DiagnosticCollector; 51 import javax.tools.DiagnosticListener; 52 import javax.tools.JavaFileManager; 53 import javax.tools.JavaFileObject; 54 import javax.tools.JavaFileObject.Kind; 55 import javax.tools.SimpleJavaFileObject; 56 57 /** {@code JavaInput} extends {@link Input} to represent a Java input document. */ 58 public final class JavaInput extends Input { 59 /** 60 * A {@code JavaInput} is a sequence of {@link Tok}s that cover the Java input. A {@link Tok} is 61 * either a token (if {@code isToken()}), or a non-token, which is a comment (if {@code 62 * isComment()}) or a newline (if {@code isNewline()}) or a maximal sequence of other whitespace 63 * characters (if {@code isSpaces()}). Each {@link Tok} contains a sequence of characters, an 64 * index (sequential starting at {@code 0} for tokens and comments, else {@code -1}), and a 65 * ({@code 0}-origin) position in the input. The concatenation of the texts of all the {@link 66 * Tok}s equals the input. Each Input ends with a token EOF {@link Tok}, with empty text. 67 * 68 * <p>A {@code /*} comment possibly contains newlines; a {@code //} comment does not contain the 69 * terminating newline character, but is followed by a newline {@link Tok}. 70 */ 71 static final class Tok implements Input.Tok { 72 private final int index; 73 private final String originalText; 74 private final String text; 75 private final int position; 76 private final int columnI; 77 private final boolean isToken; 78 private final TokenKind kind; 79 80 /** 81 * The {@code Tok} constructor. 82 * 83 * @param index its index 84 * @param originalText its original text, before removing Unicode escapes 85 * @param text its text after removing Unicode escapes 86 * @param position its {@code 0}-origin position in the input 87 * @param columnI its {@code 0}-origin column number in the input 88 * @param isToken whether the {@code Tok} is a token 89 * @param kind the token kind 90 */ Tok( int index, String originalText, String text, int position, int columnI, boolean isToken, TokenKind kind)91 Tok( 92 int index, 93 String originalText, 94 String text, 95 int position, 96 int columnI, 97 boolean isToken, 98 TokenKind kind) { 99 this.index = index; 100 this.originalText = originalText; 101 this.text = text; 102 this.position = position; 103 this.columnI = columnI; 104 this.isToken = isToken; 105 this.kind = kind; 106 } 107 108 @Override getIndex()109 public int getIndex() { 110 return index; 111 } 112 113 @Override getText()114 public String getText() { 115 return text; 116 } 117 118 @Override getOriginalText()119 public String getOriginalText() { 120 return originalText; 121 } 122 123 @Override length()124 public int length() { 125 return originalText.length(); 126 } 127 128 @Override getPosition()129 public int getPosition() { 130 return position; 131 } 132 133 @Override getColumn()134 public int getColumn() { 135 return columnI; 136 } 137 isToken()138 boolean isToken() { 139 return isToken; 140 } 141 142 @Override isNewline()143 public boolean isNewline() { 144 return Newlines.isNewline(text); 145 } 146 147 @Override isSlashSlashComment()148 public boolean isSlashSlashComment() { 149 return text.startsWith("//"); 150 } 151 152 @Override isSlashStarComment()153 public boolean isSlashStarComment() { 154 return text.startsWith("/*"); 155 } 156 157 @Override isJavadocComment()158 public boolean isJavadocComment() { 159 // comments like `/***` are also javadoc, but their formatting probably won't be improved 160 // by the javadoc formatter 161 return text.startsWith("/**") && text.charAt("/**".length()) != '*' && text.length() > 4; 162 } 163 164 @Override isComment()165 public boolean isComment() { 166 return isSlashSlashComment() || isSlashStarComment(); 167 } 168 169 @Override toString()170 public String toString() { 171 return MoreObjects.toStringHelper(this) 172 .add("index", index) 173 .add("text", text) 174 .add("position", position) 175 .add("columnI", columnI) 176 .add("isToken", isToken) 177 .toString(); 178 } 179 kind()180 public TokenKind kind() { 181 return kind; 182 } 183 } 184 185 /** 186 * A {@link Token} contains a token {@link Tok} and its associated non-tokens; each non-token 187 * {@link Tok} belongs to one {@link Token}. Each {@link Token} has an immutable list of its 188 * non-tokens that appear before it, and another list of its non-tokens that appear after it. The 189 * concatenation of the texts of all the {@link Token}s' {@link Tok}s, each preceded by the texts 190 * of its {@code toksBefore} and followed by the texts of its {@code toksAfter}, equals the input. 191 */ 192 static final class Token implements Input.Token { 193 private final Tok tok; 194 private final ImmutableList<Tok> toksBefore; 195 private final ImmutableList<Tok> toksAfter; 196 197 /** 198 * Token constructor. 199 * 200 * @param toksBefore the earlier non-token {link Tok}s assigned to this {@code Token} 201 * @param tok this token {@link Tok} 202 * @param toksAfter the later non-token {link Tok}s assigned to this {@code Token} 203 */ Token(List<Tok> toksBefore, Tok tok, List<Tok> toksAfter)204 Token(List<Tok> toksBefore, Tok tok, List<Tok> toksAfter) { 205 this.toksBefore = ImmutableList.copyOf(toksBefore); 206 this.tok = tok; 207 this.toksAfter = ImmutableList.copyOf(toksAfter); 208 } 209 210 /** 211 * Get the token's {@link Tok}. 212 * 213 * @return the token's {@link Tok} 214 */ 215 @Override getTok()216 public Tok getTok() { 217 return tok; 218 } 219 220 /** 221 * Get the earlier {@link Tok}s assigned to this {@code Token}. 222 * 223 * @return the earlier {@link Tok}s assigned to this {@code Token} 224 */ 225 @Override getToksBefore()226 public ImmutableList<? extends Input.Tok> getToksBefore() { 227 return toksBefore; 228 } 229 230 /** 231 * Get the later {@link Tok}s assigned to this {@code Token}. 232 * 233 * @return the later {@link Tok}s assigned to this {@code Token} 234 */ 235 @Override getToksAfter()236 public ImmutableList<? extends Input.Tok> getToksAfter() { 237 return toksAfter; 238 } 239 240 @Override toString()241 public String toString() { 242 return MoreObjects.toStringHelper(this) 243 .add("tok", tok) 244 .add("toksBefore", toksBefore) 245 .add("toksAfter", toksAfter) 246 .toString(); 247 } 248 } 249 250 private final String text; // The input. 251 private int kN; // The number of numbered toks (tokens or comments), excluding the EOF. 252 253 /* 254 * The following lists record the sequential indices of the {@code Tok}s on each input line. (Only 255 * tokens and comments have sequential indices.) Tokens and {@code //} comments lie on just one 256 * line; {@code /*} comments can lie on multiple lines. These data structures (along with 257 * equivalent ones for the formatted output) let us compute correspondences between the input and 258 * output. 259 */ 260 261 private final ImmutableMap<Integer, Integer> positionToColumnMap; // Map Tok position to column. 262 private final ImmutableList<Token> tokens; // The Tokens for this input. 263 private final ImmutableRangeMap<Integer, Token> positionTokenMap; // Map position to Token. 264 265 /** Map from Tok index to the associated Token. */ 266 private final Token[] kToToken; 267 268 /** 269 * Input constructor. 270 * 271 * @param text the input text 272 * @throws FormatterException if the input cannot be parsed 273 */ JavaInput(String text)274 public JavaInput(String text) throws FormatterException { 275 this.text = checkNotNull(text); 276 setLines(ImmutableList.copyOf(Newlines.lineIterator(text))); 277 ImmutableList<Tok> toks = buildToks(text); 278 positionToColumnMap = makePositionToColumnMap(toks); 279 tokens = buildTokens(toks); 280 ImmutableRangeMap.Builder<Integer, Token> tokenLocations = ImmutableRangeMap.builder(); 281 for (Token token : tokens) { 282 Input.Tok end = JavaOutput.endTok(token); 283 int upper = end.getPosition(); 284 if (!end.getText().isEmpty()) { 285 upper += end.length() - 1; 286 } 287 tokenLocations.put(Range.closed(JavaOutput.startTok(token).getPosition(), upper), token); 288 } 289 positionTokenMap = tokenLocations.build(); 290 291 // adjust kN for EOF 292 kToToken = new Token[kN + 1]; 293 for (Token token : tokens) { 294 for (Input.Tok tok : token.getToksBefore()) { 295 if (tok.getIndex() < 0) { 296 continue; 297 } 298 kToToken[tok.getIndex()] = token; 299 } 300 kToToken[token.getTok().getIndex()] = token; 301 for (Input.Tok tok : token.getToksAfter()) { 302 if (tok.getIndex() < 0) { 303 continue; 304 } 305 kToToken[tok.getIndex()] = token; 306 } 307 } 308 } 309 makePositionToColumnMap(List<Tok> toks)310 private static ImmutableMap<Integer, Integer> makePositionToColumnMap(List<Tok> toks) { 311 ImmutableMap.Builder<Integer, Integer> builder = ImmutableMap.builder(); 312 for (Tok tok : toks) { 313 builder.put(tok.getPosition(), tok.getColumn()); 314 } 315 return builder.buildOrThrow(); 316 } 317 318 /** 319 * Get the input text. 320 * 321 * @return the input text 322 */ 323 @Override getText()324 public String getText() { 325 return text; 326 } 327 328 @Override getPositionToColumnMap()329 public ImmutableMap<Integer, Integer> getPositionToColumnMap() { 330 return positionToColumnMap; 331 } 332 333 /** Lex the input and build the list of toks. */ buildToks(String text)334 private ImmutableList<Tok> buildToks(String text) throws FormatterException { 335 ImmutableList<Tok> toks = buildToks(text, ImmutableSet.of()); 336 kN = getLast(toks).getIndex(); 337 computeRanges(toks); 338 return toks; 339 } 340 341 /** 342 * Lex the input and build the list of toks. 343 * 344 * @param text the text to be lexed. 345 * @param stopTokens a set of tokens which should cause lexing to stop. If one of these is found, 346 * the returned list will include tokens up to but not including that token. 347 */ buildToks(String text, ImmutableSet<TokenKind> stopTokens)348 static ImmutableList<Tok> buildToks(String text, ImmutableSet<TokenKind> stopTokens) 349 throws FormatterException { 350 stopTokens = ImmutableSet.<TokenKind>builder().addAll(stopTokens).add(TokenKind.EOF).build(); 351 Context context = new Context(); 352 Options.instance(context).put("--enable-preview", "true"); 353 JavaFileManager fileManager = new JavacFileManager(context, false, UTF_8); 354 context.put(JavaFileManager.class, fileManager); 355 DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<>(); 356 context.put(DiagnosticListener.class, diagnosticCollector); 357 Log log = Log.instance(context); 358 log.useSource( 359 new SimpleJavaFileObject(URI.create("Source.java"), Kind.SOURCE) { 360 @Override 361 public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { 362 return text; 363 } 364 }); 365 DeferredDiagnosticHandler diagnostics = new DeferredDiagnosticHandler(log); 366 ImmutableList<RawTok> rawToks = JavacTokens.getTokens(text, context, stopTokens); 367 if (diagnostics.getDiagnostics().stream().anyMatch(d -> d.getKind() == Diagnostic.Kind.ERROR)) { 368 return ImmutableList.of(new Tok(0, "", "", 0, 0, true, null)); // EOF 369 } 370 int kN = 0; 371 List<Tok> toks = new ArrayList<>(); 372 int charI = 0; 373 int columnI = 0; 374 for (RawTok t : rawToks) { 375 if (stopTokens.contains(t.kind())) { 376 break; 377 } 378 int charI0 = t.pos(); 379 // Get string, possibly with Unicode escapes. 380 String originalTokText = text.substring(charI0, t.endPos()); 381 String tokText = 382 t.kind() == TokenKind.STRINGLITERAL 383 ? t.stringVal() // Unicode escapes removed. 384 : originalTokText; 385 char tokText0 = tokText.charAt(0); // The token's first character. 386 final boolean isToken; // Is this tok a token? 387 final boolean isNumbered; // Is this tok numbered? (tokens and comments) 388 String extraNewline = null; // Extra newline at end? 389 List<String> strings = new ArrayList<>(); 390 if (tokText.startsWith("'") 391 || tokText.startsWith("\"") 392 || JavacTokens.isStringFragment(t.kind())) { 393 // Perform this check first, STRINGFRAGMENT tokens can start with arbitrary characters. 394 isToken = true; 395 isNumbered = true; 396 strings.add(originalTokText); 397 } else if (Character.isWhitespace(tokText0)) { 398 isToken = false; 399 isNumbered = false; 400 Iterator<String> it = Newlines.lineIterator(originalTokText); 401 while (it.hasNext()) { 402 String line = it.next(); 403 String newline = Newlines.getLineEnding(line); 404 if (newline != null) { 405 String spaces = line.substring(0, line.length() - newline.length()); 406 if (!spaces.isEmpty()) { 407 strings.add(spaces); 408 } 409 strings.add(newline); 410 } else if (!line.isEmpty()) { 411 strings.add(line); 412 } 413 } 414 } else if (tokText.startsWith("//") || tokText.startsWith("/*")) { 415 // For compatibility with an earlier lexer, the newline after a // comment is its own tok. 416 if (tokText.startsWith("//") 417 && (originalTokText.endsWith("\n") || originalTokText.endsWith("\r"))) { 418 extraNewline = Newlines.getLineEnding(originalTokText); 419 tokText = tokText.substring(0, tokText.length() - extraNewline.length()); 420 originalTokText = 421 originalTokText.substring(0, originalTokText.length() - extraNewline.length()); 422 } 423 isToken = false; 424 isNumbered = true; 425 strings.add(originalTokText); 426 } else if (Character.isJavaIdentifierStart(tokText0) 427 || Character.isDigit(tokText0) 428 || (tokText0 == '.' && tokText.length() > 1 && Character.isDigit(tokText.charAt(1)))) { 429 // Identifier, keyword, or numeric literal (a dot may begin a number, as in .2D). 430 isToken = true; 431 isNumbered = true; 432 strings.add(tokText); 433 } else { 434 // Other tokens ("+" or "++" or ">>" are broken into one-character toks, because ">>" 435 // cannot be lexed without syntactic knowledge. This implementation fails if the token 436 // contains Unicode escapes. 437 isToken = true; 438 isNumbered = true; 439 for (char c : tokText.toCharArray()) { 440 strings.add(String.valueOf(c)); 441 } 442 } 443 if (strings.size() == 1) { 444 toks.add( 445 new Tok( 446 isNumbered ? kN++ : -1, 447 originalTokText, 448 tokText, 449 charI, 450 columnI, 451 isToken, 452 t.kind())); 453 charI += originalTokText.length(); 454 columnI = updateColumn(columnI, originalTokText); 455 456 } else { 457 if (strings.size() != 1 && !tokText.equals(originalTokText)) { 458 throw new FormatterException( 459 "Unicode escapes not allowed in whitespace or multi-character operators"); 460 } 461 for (String str : strings) { 462 toks.add(new Tok(isNumbered ? kN++ : -1, str, str, charI, columnI, isToken, null)); 463 charI += str.length(); 464 columnI = updateColumn(columnI, originalTokText); 465 } 466 } 467 if (extraNewline != null) { 468 toks.add(new Tok(-1, extraNewline, extraNewline, charI, columnI, false, null)); 469 columnI = 0; 470 charI += extraNewline.length(); 471 } 472 } 473 toks.add(new Tok(kN, "", "", charI, columnI, true, null)); // EOF tok. 474 return ImmutableList.copyOf(toks); 475 } 476 updateColumn(int columnI, String originalTokText)477 private static int updateColumn(int columnI, String originalTokText) { 478 Integer last = Iterators.getLast(Newlines.lineOffsetIterator(originalTokText)); 479 if (last > 0) { 480 columnI = originalTokText.length() - last; 481 } else { 482 columnI += originalTokText.length(); 483 } 484 return columnI; 485 } 486 buildTokens(List<Tok> toks)487 private static ImmutableList<Token> buildTokens(List<Tok> toks) { 488 ImmutableList.Builder<Token> tokens = ImmutableList.builder(); 489 int k = 0; 490 int kN = toks.size(); 491 492 // Remaining non-tokens before the token go here. 493 ImmutableList.Builder<Tok> toksBefore = ImmutableList.builder(); 494 495 OUTERMOST: 496 while (k < kN) { 497 while (!toks.get(k).isToken()) { 498 Tok tok = toks.get(k++); 499 toksBefore.add(tok); 500 if (isParamComment(tok)) { 501 while (toks.get(k).isNewline()) { 502 // drop newlines after parameter comments 503 k++; 504 } 505 } 506 } 507 Tok tok = toks.get(k++); 508 509 // Non-tokens starting on the same line go here too. 510 ImmutableList.Builder<Tok> toksAfter = ImmutableList.builder(); 511 OUTER: 512 while (k < kN && !toks.get(k).isToken()) { 513 // Don't attach inline comments to certain leading tokens, e.g. for `f(/*flag1=*/true). 514 // 515 // Attaching inline comments to the right token is hard, and this barely 516 // scratches the surface. But it's enough to do a better job with parameter 517 // name comments. 518 // 519 // TODO(cushon): find a better strategy. 520 if (toks.get(k).isSlashStarComment()) { 521 switch (tok.getText()) { 522 case "(": 523 case "<": 524 case ".": 525 break OUTER; 526 default: 527 break; 528 } 529 } 530 if (toks.get(k).isJavadocComment()) { 531 switch (tok.getText()) { 532 case ";": 533 break OUTER; 534 default: 535 break; 536 } 537 } 538 if (isParamComment(toks.get(k))) { 539 tokens.add(new Token(toksBefore.build(), tok, toksAfter.build())); 540 toksBefore = ImmutableList.<Tok>builder().add(toks.get(k++)); 541 // drop newlines after parameter comments 542 while (toks.get(k).isNewline()) { 543 k++; 544 } 545 continue OUTERMOST; 546 } 547 Tok nonTokenAfter = toks.get(k++); 548 toksAfter.add(nonTokenAfter); 549 if (Newlines.containsBreaks(nonTokenAfter.getText())) { 550 break; 551 } 552 } 553 tokens.add(new Token(toksBefore.build(), tok, toksAfter.build())); 554 toksBefore = ImmutableList.builder(); 555 } 556 return tokens.build(); 557 } 558 isParamComment(Tok tok)559 private static boolean isParamComment(Tok tok) { 560 return tok.isSlashStarComment() 561 && tok.getText().matches("\\/\\*[A-Za-z0-9\\s_\\-]+=\\s*\\*\\/"); 562 } 563 564 /** 565 * Convert from a character range to a token range. 566 * 567 * @param characterRange the {@code 0}-based {@link Range} of characters 568 * @return the {@code 0}-based {@link Range} of tokens 569 * @throws FormatterException if the upper endpoint of the range is outside the file 570 */ characterRangeToTokenRange(Range<Integer> characterRange)571 Range<Integer> characterRangeToTokenRange(Range<Integer> characterRange) 572 throws FormatterException { 573 if (characterRange.upperEndpoint() > text.length()) { 574 throw new FormatterException( 575 String.format( 576 "error: invalid length %d, offset + length (%d) is outside the file", 577 characterRange.upperEndpoint() - characterRange.lowerEndpoint(), 578 characterRange.upperEndpoint())); 579 } 580 // empty range stands for "format the line under the cursor" 581 Range<Integer> nonEmptyRange = 582 characterRange.isEmpty() 583 ? Range.closedOpen(characterRange.lowerEndpoint(), characterRange.lowerEndpoint() + 1) 584 : characterRange; 585 ImmutableCollection<Token> enclosed = 586 getPositionTokenMap().subRangeMap(nonEmptyRange).asMapOfRanges().values(); 587 if (enclosed.isEmpty()) { 588 return EMPTY_RANGE; 589 } 590 return Range.closedOpen( 591 enclosed.iterator().next().getTok().getIndex(), getLast(enclosed).getTok().getIndex() + 1); 592 } 593 594 /** 595 * Get the number of toks. 596 * 597 * @return the number of toks, excluding the EOF tok 598 */ 599 @Override getkN()600 public int getkN() { 601 return kN; 602 } 603 604 /** 605 * Get the Token by index. 606 * 607 * @param k the Tok index 608 */ 609 @Override getToken(int k)610 public Token getToken(int k) { 611 return kToToken[k]; 612 } 613 614 /** 615 * Get the input tokens. 616 * 617 * @return the input tokens 618 */ 619 @Override getTokens()620 public ImmutableList<? extends Input.Token> getTokens() { 621 return tokens; 622 } 623 624 /** 625 * Get the navigable map from position to {@link Token}. Used to look for tokens following a given 626 * one, and to implement the --offset and --length flags to reformat a character range in the 627 * input file. 628 * 629 * @return the navigable map from position to {@link Token} 630 */ 631 @Override getPositionTokenMap()632 public ImmutableRangeMap<Integer, Token> getPositionTokenMap() { 633 return positionTokenMap; 634 } 635 636 @Override toString()637 public String toString() { 638 return MoreObjects.toStringHelper(this) 639 .add("tokens", tokens) 640 .add("super", super.toString()) 641 .toString(); 642 } 643 644 private JCCompilationUnit unit; 645 646 @Override getLineNumber(int inputPosition)647 public int getLineNumber(int inputPosition) { 648 Verify.verifyNotNull(unit, "Expected compilation unit to be set."); 649 return unit.getLineMap().getLineNumber(inputPosition); 650 } 651 652 @Override getColumnNumber(int inputPosition)653 public int getColumnNumber(int inputPosition) { 654 Verify.verifyNotNull(unit, "Expected compilation unit to be set."); 655 return unit.getLineMap().getColumnNumber(inputPosition); 656 } 657 658 // TODO(cushon): refactor JavaInput so the CompilationUnit can be passed into 659 // the constructor. setCompilationUnit(JCCompilationUnit unit)660 public void setCompilationUnit(JCCompilationUnit unit) { 661 this.unit = unit; 662 } 663 characterRangesToTokenRanges(Collection<Range<Integer>> characterRanges)664 public RangeSet<Integer> characterRangesToTokenRanges(Collection<Range<Integer>> characterRanges) 665 throws FormatterException { 666 RangeSet<Integer> tokenRangeSet = TreeRangeSet.create(); 667 for (Range<Integer> characterRange : characterRanges) { 668 tokenRangeSet.add( 669 characterRangeToTokenRange(characterRange.canonical(DiscreteDomain.integers()))); 670 } 671 return tokenRangeSet; 672 } 673 } 674