xref: /aosp_15_r20/external/google-java-format/core/src/main/java/com/google/googlejavaformat/java/JavaInput.java (revision 10816b529e1d7005ca788e7b4c5efd1c72957e26)
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