1 /* 2 * Copyright 2017 Google LLC 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.auto.value.processor; 17 18 import static com.google.common.base.Preconditions.checkArgument; 19 import static com.google.common.base.Preconditions.checkState; 20 import static java.util.stream.Collectors.toList; 21 22 import com.google.auto.common.MoreElements; 23 import com.google.auto.common.MoreTypes; 24 import com.google.auto.value.processor.MissingTypes.MissingTypeException; 25 import com.google.common.collect.ImmutableList; 26 import com.google.common.collect.ImmutableSet; 27 import java.util.List; 28 import java.util.OptionalInt; 29 import java.util.Set; 30 import java.util.function.Function; 31 import javax.annotation.processing.ProcessingEnvironment; 32 import javax.lang.model.element.AnnotationMirror; 33 import javax.lang.model.element.TypeElement; 34 import javax.lang.model.element.TypeParameterElement; 35 import javax.lang.model.type.ArrayType; 36 import javax.lang.model.type.DeclaredType; 37 import javax.lang.model.type.ErrorType; 38 import javax.lang.model.type.PrimitiveType; 39 import javax.lang.model.type.TypeKind; 40 import javax.lang.model.type.TypeMirror; 41 import javax.lang.model.type.TypeVariable; 42 import javax.lang.model.type.WildcardType; 43 import javax.lang.model.util.Elements; 44 import javax.lang.model.util.SimpleTypeVisitor8; 45 import javax.lang.model.util.Types; 46 47 /** 48 * Encodes types so they can later be decoded to incorporate imports. 49 * 50 * <p>The idea is that types that appear in generated source code use {@link #encode}, which will 51 * spell out a type like {@code java.util.List<? extends java.lang.Number>}, except that wherever a 52 * class name appears it is replaced by a special token. So the spelling might actually be {@code 53 * `java.util.List`<? extends `java.lang.Number`>}. Then once the entire class has been generated, 54 * {@code #decode} scans for these tokens to determine what classes need to be imported, and 55 * replaces the tokens with the correct spelling given the imports. So here, {@code java.util.List} 56 * would be imported, and the final spelling would be {@code List<? extends Number>} (knowing that 57 * {@code Number} is implicitly imported). The special token {@code `import`} marks where the 58 * imports should be, and {@link #decode} replaces it with the correct list of imports. 59 * 60 * <p>The funky syntax for type annotations on qualified type names requires an adjustment to this 61 * scheme. {@code `«java.util.Map`} stands for {@code java.util.} and {@code `»java.util.Map`} 62 * stands for {@code Map}. If {@code java.util.Map} is imported, then {@code `«java.util.Map`} will 63 * eventually be empty, but if {@code java.util.Map} is not imported (perhaps because there is 64 * another {@code Map} in scope) then {@code `«java.util.Map`} will be {@code java.util.}. The end 65 * result is that the code can contain {@code `«java.util.Map`@`javax.annotation.Nullable` 66 * `»java.util.Map`}. That might decode to {@code @Nullable Map} or to {@code java.util.@Nullable 67 * Map} or even to {@code java.util.@javax.annotation.Nullable Map}. 68 * 69 * @author emcmanus@google.com (Éamonn McManus) 70 */ 71 final class TypeEncoder { TypeEncoder()72 private TypeEncoder() {} // There are no instances of this class. 73 74 private static final EncodingTypeVisitor ENCODING_TYPE_VISITOR = new EncodingTypeVisitor(); 75 private static final RawEncodingTypeVisitor RAW_ENCODING_TYPE_VISITOR = 76 new RawEncodingTypeVisitor(); 77 78 /** 79 * Returns the encoding for the given type, where class names are marked by special tokens. The 80 * encoding for {@code int} will be {@code int}, but the encoding for {@code 81 * java.util.List<java.lang.Integer>} will be {@code `java.util.List`<`java.lang.Integer`>}. 82 */ encode(TypeMirror type)83 static String encode(TypeMirror type) { 84 StringBuilder sb = new StringBuilder(); 85 return type.accept(ENCODING_TYPE_VISITOR, sb).toString(); 86 } 87 88 /** 89 * Like {@link #encode}, except that only the raw type is encoded. So if the given type is {@code 90 * java.util.List<java.lang.Integer>} the result will be {@code `java.util.List`}. 91 */ encodeRaw(TypeMirror type)92 static String encodeRaw(TypeMirror type) { 93 StringBuilder sb = new StringBuilder(); 94 return type.accept(RAW_ENCODING_TYPE_VISITOR, sb).toString(); 95 } 96 97 /** 98 * Encodes the given type and its type annotations. The class comment for {@link TypeEncoder} 99 * covers the details of annotation encoding. 100 */ encodeWithAnnotations(TypeMirror type)101 static String encodeWithAnnotations(TypeMirror type) { 102 return encodeWithAnnotations(type, ImmutableList.of(), ImmutableSet.of()); 103 } 104 105 /** 106 * Encodes the given type and its type annotations. The class comment for {@link TypeEncoder} 107 * covers the details of annotation encoding. 108 * 109 * @param extraAnnotations additional type annotations to include with the type 110 * @param excludedAnnotationTypes annotations not to include in the encoding. For example, if 111 * {@code com.example.Nullable} is in this set then the encoding will not include this 112 * {@code @Nullable} annotation. 113 */ encodeWithAnnotations( TypeMirror type, ImmutableList<AnnotationMirror> extraAnnotations, Set<TypeMirror> excludedAnnotationTypes)114 static String encodeWithAnnotations( 115 TypeMirror type, 116 ImmutableList<AnnotationMirror> extraAnnotations, 117 Set<TypeMirror> excludedAnnotationTypes) { 118 StringBuilder sb = new StringBuilder(); 119 // A function that is equivalent to t.getAnnotationMirrors() except when the t in question is 120 // our starting type. In that case we also add extraAnnotations to the result. 121 Function<TypeMirror, List<? extends AnnotationMirror>> getTypeAnnotations = 122 t -> 123 (t == type) 124 ? ImmutableList.<AnnotationMirror>builder() 125 .addAll(t.getAnnotationMirrors()) 126 .addAll(extraAnnotations) 127 .build() 128 : t.getAnnotationMirrors(); 129 return new AnnotatedEncodingTypeVisitor(excludedAnnotationTypes, getTypeAnnotations) 130 .visit2(type, sb) 131 .toString(); 132 } 133 134 /** 135 * Decodes the given string, respelling class names appropriately. The text is scanned for tokens 136 * like {@code `java.util.Locale`} or {@code `«java.util.Locale`} to determine which classes are 137 * referenced. An appropriate set of imports is computed based on the set of those types. If the 138 * special token {@code `import`} appears in {@code text} then it will be replaced by this set of 139 * import statements. Then all of the tokens are replaced by the class names they represent, 140 * spelled appropriately given the import statements. 141 * 142 * @param text the text to be decoded. 143 * @param packageName the package of the generated class. Other classes in the same package do not 144 * need to be imported. 145 * @param baseType a class or interface that the generated class inherits from. Nested classes in 146 * that type do not need to be imported, and if another class has the same name as one of 147 * those nested classes then it will need to be qualified. 148 */ decode( String text, ProcessingEnvironment processingEnv, String packageName, TypeMirror baseType)149 static String decode( 150 String text, ProcessingEnvironment processingEnv, String packageName, TypeMirror baseType) { 151 return decode( 152 text, processingEnv.getElementUtils(), processingEnv.getTypeUtils(), packageName, baseType); 153 } 154 decode( String text, Elements elementUtils, Types typeUtils, String pkg, TypeMirror baseType)155 static String decode( 156 String text, Elements elementUtils, Types typeUtils, String pkg, TypeMirror baseType) { 157 TypeRewriter typeRewriter = new TypeRewriter(text, elementUtils, typeUtils, pkg, baseType); 158 return typeRewriter.rewrite(); 159 } 160 className(DeclaredType declaredType)161 private static String className(DeclaredType declaredType) { 162 return MoreElements.asType(declaredType.asElement()).getQualifiedName().toString(); 163 } 164 165 /** 166 * Returns a string representing the given type parameters as they would appear in a class 167 * declaration. For example, if we have {@code @AutoValue abstract 168 * class Foo<T extends SomeClass>} then if we call {@link TypeElement#getTypeParameters()} on 169 * the representation of {@code Foo}, this method will return an encoding of {@code <T extends 170 * SomeClass>}. Likewise it will return an encoding of the angle-bracket part of: 171 * <br> 172 * {@code Foo<SomeClass>}<br> 173 * {@code Foo<T extends Number>}<br> 174 * {@code Foo<E extends Enum<E>>}<br> 175 * {@code Foo<K, V extends Comparable<? extends K>>}. 176 * 177 * <p>The encoding is simply that classes in the "extends" part are marked, so the examples will 178 * actually look something like this:<br> 179 * {@code <`bar.baz.SomeClass`>}<br> 180 * {@code <T extends `java.lang.Number`>}<br> 181 * {@code <E extends `java.lang.Enum`<E>>}<br> 182 * {@code <K, V extends `java.lang.Comparable`<? extends K>>}. 183 */ typeParametersString(List<? extends TypeParameterElement> typeParameters)184 static String typeParametersString(List<? extends TypeParameterElement> typeParameters) { 185 if (typeParameters.isEmpty()) { 186 return ""; 187 } else { 188 StringBuilder sb = new StringBuilder("<"); 189 String sep = ""; 190 for (TypeParameterElement typeParameter : typeParameters) { 191 sb.append(sep); 192 sep = ", "; 193 appendTypeParameterWithBounds(typeParameter, sb); 194 } 195 return sb.append(">").toString(); 196 } 197 } 198 appendTypeParameterWithBounds( TypeParameterElement typeParameter, StringBuilder sb)199 private static void appendTypeParameterWithBounds( 200 TypeParameterElement typeParameter, StringBuilder sb) { 201 appendAnnotations(typeParameter.getAnnotationMirrors(), sb); 202 sb.append(typeParameter.getSimpleName()); 203 String sep = " extends "; 204 for (TypeMirror bound : typeParameter.getBounds()) { 205 if (!isUnannotatedJavaLangObject(bound)) { 206 sb.append(sep); 207 sep = " & "; 208 sb.append(encodeWithAnnotations(bound)); 209 } 210 } 211 } 212 213 // We can omit "extends Object" from a type bound, but not "extends @NullableType Object". isUnannotatedJavaLangObject(TypeMirror type)214 private static boolean isUnannotatedJavaLangObject(TypeMirror type) { 215 return type.getKind().equals(TypeKind.DECLARED) 216 && type.getAnnotationMirrors().isEmpty() 217 && MoreTypes.asTypeElement(type).getQualifiedName().contentEquals("java.lang.Object"); 218 } 219 appendAnnotations( List<? extends AnnotationMirror> annotationMirrors, StringBuilder sb)220 private static void appendAnnotations( 221 List<? extends AnnotationMirror> annotationMirrors, StringBuilder sb) { 222 for (AnnotationMirror annotationMirror : annotationMirrors) { 223 sb.append(AnnotationOutput.sourceFormForAnnotation(annotationMirror)).append(" "); 224 } 225 } 226 227 /** 228 * Converts a type into a string, using standard Java syntax, except that every class name is 229 * wrapped in backquotes, like {@code `java.util.List`}. 230 */ 231 private static class EncodingTypeVisitor 232 extends SimpleTypeVisitor8<StringBuilder, StringBuilder> { 233 /** 234 * Equivalent to {@code visit(type, sb)} or {@code type.accept(sb)}, except that it fixes a bug 235 * with javac versions up to JDK 8, whereby if the type is a {@code DeclaredType} then the 236 * visitor is called with a version of the type where any annotations have been lost. We can't 237 * override {@code visit} because it is final. 238 */ visit2(TypeMirror type, StringBuilder sb)239 StringBuilder visit2(TypeMirror type, StringBuilder sb) { 240 if (type.getKind().equals(TypeKind.DECLARED)) { 241 // There's no point in using MoreTypes.asDeclared here, and in fact we can't, because it 242 // uses a visitor, so it would trigger the bug we're working around. 243 return visitDeclared((DeclaredType) type, sb); 244 } else { 245 return visit(type, sb); 246 } 247 } 248 249 @Override defaultAction(TypeMirror type, StringBuilder sb)250 protected StringBuilder defaultAction(TypeMirror type, StringBuilder sb) { 251 return sb.append(type); 252 } 253 254 @Override visitArray(ArrayType type, StringBuilder sb)255 public StringBuilder visitArray(ArrayType type, StringBuilder sb) { 256 return visit2(type.getComponentType(), sb).append("[]"); 257 } 258 259 @Override visitDeclared(DeclaredType type, StringBuilder sb)260 public StringBuilder visitDeclared(DeclaredType type, StringBuilder sb) { 261 appendTypeName(type, sb); 262 appendTypeArguments(type, sb); 263 return sb; 264 } 265 appendTypeName(DeclaredType type, StringBuilder sb)266 void appendTypeName(DeclaredType type, StringBuilder sb) { 267 TypeMirror enclosing = EclipseHack.getEnclosingType(type); 268 if (enclosing.getKind().equals(TypeKind.DECLARED)) { 269 // We might have something like com.example.Outer<Double>.Inner. We need to encode 270 // com.example.Outer<Double> first, producing `com.example.Outer`<`java.lang.Double`>. 271 // Then we can simply add .Inner after that. If Inner has its own type arguments, we'll 272 // add them with appendTypeArguments below. Of course, it's more usual for the outer class 273 // not to have type arguments, but we'll still follow this path if the nested class is an 274 // inner (not static) class. 275 visit2(enclosing, sb); 276 sb.append(".").append(type.asElement().getSimpleName()); 277 } else { 278 sb.append('`').append(className(type)).append('`'); 279 } 280 } 281 appendTypeArguments(DeclaredType type, StringBuilder sb)282 void appendTypeArguments(DeclaredType type, StringBuilder sb) { 283 List<? extends TypeMirror> arguments = type.getTypeArguments(); 284 if (!arguments.isEmpty()) { 285 sb.append("<"); 286 String sep = ""; 287 for (TypeMirror argument : arguments) { 288 sb.append(sep); 289 sep = ", "; 290 visit2(argument, sb); 291 } 292 sb.append(">"); 293 } 294 } 295 296 @Override visitWildcard(WildcardType type, StringBuilder sb)297 public StringBuilder visitWildcard(WildcardType type, StringBuilder sb) { 298 sb.append("?"); 299 TypeMirror extendsBound = type.getExtendsBound(); 300 TypeMirror superBound = type.getSuperBound(); 301 if (superBound != null) { 302 sb.append(" super "); 303 visit2(superBound, sb); 304 } else if (extendsBound != null) { 305 sb.append(" extends "); 306 visit2(extendsBound, sb); 307 } 308 return sb; 309 } 310 311 @Override visitError(ErrorType t, StringBuilder p)312 public StringBuilder visitError(ErrorType t, StringBuilder p) { 313 throw new MissingTypeException(t); 314 } 315 } 316 317 /** Like {@link EncodingTypeVisitor} except that type parameters are omitted from the result. */ 318 private static class RawEncodingTypeVisitor extends EncodingTypeVisitor { 319 @Override appendTypeArguments(DeclaredType type, StringBuilder sb)320 void appendTypeArguments(DeclaredType type, StringBuilder sb) {} 321 } 322 323 /** 324 * Like {@link EncodingTypeVisitor} except that annotations on the visited type are also included 325 * in the resultant string. Class names in those annotations are also encoded using the {@code 326 * `java.util.List`} form. 327 */ 328 private static class AnnotatedEncodingTypeVisitor extends EncodingTypeVisitor { 329 private final Set<TypeMirror> excludedAnnotationTypes; 330 private final Function<TypeMirror, List<? extends AnnotationMirror>> getTypeAnnotations; 331 AnnotatedEncodingTypeVisitor( Set<TypeMirror> excludedAnnotationTypes, Function<TypeMirror, List<? extends AnnotationMirror>> getTypeAnnotations)332 AnnotatedEncodingTypeVisitor( 333 Set<TypeMirror> excludedAnnotationTypes, 334 Function<TypeMirror, List<? extends AnnotationMirror>> getTypeAnnotations) { 335 this.excludedAnnotationTypes = excludedAnnotationTypes; 336 this.getTypeAnnotations = getTypeAnnotations; 337 } 338 appendAnnotationsWithExclusions( List<? extends AnnotationMirror> annotations, StringBuilder sb)339 private void appendAnnotationsWithExclusions( 340 List<? extends AnnotationMirror> annotations, StringBuilder sb) { 341 // Optimization for the very common cases where there are no annotations or there are no 342 // exclusions. 343 if (annotations.isEmpty() || excludedAnnotationTypes.isEmpty()) { 344 appendAnnotations(annotations, sb); 345 return; 346 } 347 List<AnnotationMirror> includedAnnotations = 348 annotations.stream() 349 .filter(a -> !excludedAnnotationTypes.contains(a.getAnnotationType())) 350 .collect(toList()); 351 appendAnnotations(includedAnnotations, sb); 352 } 353 354 @Override visitPrimitive(PrimitiveType type, StringBuilder sb)355 public StringBuilder visitPrimitive(PrimitiveType type, StringBuilder sb) { 356 appendAnnotationsWithExclusions(getTypeAnnotations.apply(type), sb); 357 // We can't just append type.toString(), because that will also have the annotation, but 358 // without encoding. 359 return sb.append(type.getKind().toString().toLowerCase()); 360 } 361 362 @Override visitTypeVariable(TypeVariable type, StringBuilder sb)363 public StringBuilder visitTypeVariable(TypeVariable type, StringBuilder sb) { 364 appendAnnotationsWithExclusions(getTypeAnnotations.apply(type), sb); 365 return sb.append(type.asElement().getSimpleName()); 366 } 367 368 /** 369 * {@inheritDoc} The result respects the Java syntax, whereby {@code Foo @Bar []} is an 370 * annotation on the array type itself, while {@code @Bar Foo[]} would be an annotation on the 371 * component type. 372 */ 373 @Override visitArray(ArrayType type, StringBuilder sb)374 public StringBuilder visitArray(ArrayType type, StringBuilder sb) { 375 visit2(type.getComponentType(), sb); 376 List<? extends AnnotationMirror> annotationMirrors = getTypeAnnotations.apply(type); 377 if (!annotationMirrors.isEmpty()) { 378 sb.append(" "); 379 appendAnnotationsWithExclusions(annotationMirrors, sb); 380 } 381 return sb.append("[]"); 382 } 383 384 @Override visitDeclared(DeclaredType type, StringBuilder sb)385 public StringBuilder visitDeclared(DeclaredType type, StringBuilder sb) { 386 List<? extends AnnotationMirror> annotationMirrors = getTypeAnnotations.apply(type); 387 if (annotationMirrors.isEmpty()) { 388 super.visitDeclared(type, sb); 389 } else { 390 TypeMirror enclosing = EclipseHack.getEnclosingType(type); 391 if (enclosing.getKind().equals(TypeKind.DECLARED)) { 392 // We have something like com.example.Outer<Double>.@Annot Inner. 393 // We'll recursively encode com.example.Outer<Double> first, 394 // which if it is also annotated might result in a mouthful like 395 // `«com.example.Outer`@`org.annots.Nullable``»com.example.Outer`<`java.lang.Double`> . 396 // That annotation will have been added by a recursive call to this method. 397 // Then we'll add the annotation on the .Inner class, which we know is there because 398 // annotationMirrors is not empty. That means we'll append .@`org.annots.Annot` Inner . 399 visit2(enclosing, sb); 400 sb.append("."); 401 appendAnnotationsWithExclusions(annotationMirrors, sb); 402 sb.append(type.asElement().getSimpleName()); 403 } else { 404 // This isn't an inner class, so we have the simpler (but still complicated) case of 405 // needing to place the annotation correctly in cases like java.util.@Nullable Map . 406 // See the class doc comment for an explanation of « and » here. 407 String className = className(type); 408 sb.append("`«").append(className).append("`"); 409 appendAnnotationsWithExclusions(annotationMirrors, sb); 410 sb.append("`»").append(className).append("`"); 411 } 412 appendTypeArguments(type, sb); 413 } 414 return sb; 415 } 416 } 417 418 private static class TypeRewriter { 419 private final String text; 420 private final int textLength; 421 private final JavaScanner scanner; 422 private final Elements elementUtils; 423 private final Types typeUtils; 424 private final String packageName; 425 private final TypeMirror baseType; 426 TypeRewriter( String text, Elements elementUtils, Types typeUtils, String pkg, TypeMirror baseType)427 TypeRewriter( 428 String text, Elements elementUtils, Types typeUtils, String pkg, TypeMirror baseType) { 429 this.text = text; 430 this.textLength = text.length(); 431 this.scanner = new JavaScanner(text); 432 this.elementUtils = elementUtils; 433 this.typeUtils = typeUtils; 434 this.packageName = pkg; 435 this.baseType = baseType; 436 } 437 rewrite()438 String rewrite() { 439 // Scan the text to determine what classes are referenced. 440 Set<TypeMirror> referencedClasses = findReferencedClasses(); 441 // Make a type simplifier based on these referenced types. 442 TypeSimplifier typeSimplifier = 443 new TypeSimplifier(elementUtils, typeUtils, packageName, referencedClasses, baseType); 444 445 StringBuilder output = new StringBuilder(); 446 int copyStart; 447 448 // Replace the `import` token with the import statements, if it is present. 449 OptionalInt importMarker = findImportMarker(); 450 if (importMarker.isPresent()) { 451 output.append(text, 0, importMarker.getAsInt()); 452 for (String toImport : typeSimplifier.typesToImport()) { 453 output.append("import ").append(toImport).append(";\n"); 454 } 455 copyStart = scanner.tokenEnd(importMarker.getAsInt()); 456 } else { 457 copyStart = 0; 458 } 459 460 // Replace each of the classname tokens with the appropriate spelling of the classname. 461 int token; 462 for (token = copyStart; token < textLength; token = scanner.tokenEnd(token)) { 463 if (text.charAt(token) == '`') { 464 output.append(text, copyStart, token); 465 decode(output, typeSimplifier, token); 466 copyStart = scanner.tokenEnd(token); 467 } 468 } 469 output.append(text, copyStart, textLength); 470 return output.toString(); 471 } 472 findReferencedClasses()473 private Set<TypeMirror> findReferencedClasses() { 474 Set<TypeMirror> classes = new TypeMirrorSet(); 475 for (int token = 0; token < textLength; token = scanner.tokenEnd(token)) { 476 if (text.charAt(token) == '`' && !text.startsWith("`import`", token)) { 477 String className = classNameAt(token); 478 classes.add(classForName(className)); 479 } 480 } 481 return classes; 482 } 483 classForName(String className)484 private DeclaredType classForName(String className) { 485 TypeElement typeElement = elementUtils.getTypeElement(className); 486 checkState(typeElement != null, "Could not find referenced class %s", className); 487 return MoreTypes.asDeclared(typeElement.asType()); 488 } 489 decode(StringBuilder output, TypeSimplifier typeSimplifier, int token)490 private void decode(StringBuilder output, TypeSimplifier typeSimplifier, int token) { 491 String className = classNameAt(token); 492 DeclaredType type = classForName(className); 493 String simplified = typeSimplifier.simplifiedClassName(type); 494 int dot; 495 switch (text.charAt(token + 1)) { 496 case '«': 497 // If this is `«java.util.Map` then we want "java.util." here. 498 // That's because this is the first part of something like "java.util.@Nullable Map" 499 // or "java.util.Map.@Nullable Entry". 500 // If there's no dot, then we want nothing here, for "@Nullable Map". 501 dot = simplified.lastIndexOf('.'); 502 output.append(simplified.substring(0, dot + 1)); // correct even if dot == -1 503 break; 504 case '»': 505 dot = simplified.lastIndexOf('.'); 506 output.append(simplified.substring(dot + 1)); // correct even if dot == -1 507 break; 508 default: 509 output.append(simplified); 510 break; 511 } 512 } 513 findImportMarker()514 private OptionalInt findImportMarker() { 515 for (int token = 0; token < textLength; token = scanner.tokenEnd(token)) { 516 if (text.startsWith("`import`", token)) { 517 return OptionalInt.of(token); 518 } 519 } 520 return OptionalInt.empty(); 521 } 522 classNameAt(int token)523 private String classNameAt(int token) { 524 checkArgument(text.charAt(token) == '`'); 525 int end = scanner.tokenEnd(token) - 1; // points to the closing ` 526 int t = token + 1; 527 char c = text.charAt(t); 528 if (c == '«' || c == '»') { 529 t++; 530 } 531 return text.substring(t, end); 532 } 533 } 534 } 535