1 package annotator.find; 2 3 import java.util.Iterator; 4 import java.util.LinkedHashSet; 5 import java.util.List; 6 import java.util.Set; 7 8 import annotations.io.ASTPath; 9 10 import com.sun.source.tree.Tree; 11 import com.sun.tools.javac.code.TypeAnnotationPosition.TypePathEntry; 12 13 import plume.Pair; 14 import type.ArrayType; 15 import type.DeclaredType; 16 import type.Type; 17 import type.BoundedType; 18 19 /** 20 * Specifies something that needs to be inserted into a source file, including 21 * the "what" and the "where". 22 */ 23 public abstract class Insertion { 24 25 public enum Kind { 26 ANNOTATION, 27 CAST, 28 CONSTRUCTOR, 29 METHOD, 30 NEW, 31 RECEIVER, 32 CLOSE_PARENTHESIS 33 } 34 35 private final Criteria criteria; 36 // If non-null, then try to put annotation on its own line, 37 // horizontally aligned with the location. 38 private final boolean separateLine; 39 40 /** 41 * Whether this insertion has already been inserted into source code. 42 */ 43 private boolean inserted; 44 45 /** 46 * The package names for the annotations being inserted by this Insertion. 47 * This will be empty unless {@link #getText(boolean, boolean)} is called 48 * with abbreviate true. 49 */ 50 protected Set<String> packageNames; 51 52 /** 53 * Set of annotation names that should always be qualified, even 54 * when {@link getText(boolean, boolean)} is called with abbreviate true. 55 */ 56 protected static Set<String> alwaysQualify = new LinkedHashSet<String>(); 57 58 /** 59 * Creates a new insertion. 60 * 61 * @param criteria where to insert the text 62 * @param separateLine whether to insert the text on its own 63 */ 64 public Insertion(Criteria criteria, boolean separateLine) { 65 this.criteria = criteria; 66 this.separateLine = separateLine; 67 this.packageNames = new LinkedHashSet<String>(); 68 this.inserted = false; 69 } 70 71 /** 72 * Gets the insertion criteria. 73 * 74 * @return the criteria 75 */ 76 public Criteria getCriteria() { 77 return criteria; 78 } 79 80 /** 81 * Gets the insertion text (not commented or abbreviated, and without added 82 * leading or trailing whitespace). 83 * 84 * @return the text to insert 85 */ 86 public String getText() { 87 return getText(false, false, true, 0, '\0'); 88 } 89 90 /** 91 * Gets the insertion text with a leading and/or trailing space added based 92 * on the values of the {@code gotSeparateLine}, {@code pos}, and 93 * {@code precedingChar} parameters. 94 * 95 * @param comments 96 * if true, Java 8 features will be surrounded in comments 97 * @param abbreviate 98 * if true, the package name will be removed from the annotations. 99 * The package name can be retrieved again by calling the 100 * {@link #getPackageNames()} method. 101 * @param gotSeparateLine 102 * {@code true} if this insertion is actually added on a separate 103 * line. 104 * @param pos 105 * the source position where this insertion will be inserted 106 * @param precedingChar 107 * the character directly preceding where this insertion will be 108 * inserted. This value will be ignored if {@code pos} is 0. 109 * 110 * @return the text to insert 111 */ 112 public String getText(boolean comments, boolean abbreviate, 113 boolean gotSeparateLine, int pos, char precedingChar) { 114 String toInsert = getText(comments, abbreviate); 115 if (!toInsert.isEmpty()) { 116 if (addLeadingSpace(gotSeparateLine, pos, precedingChar)) { 117 toInsert = " " + toInsert; 118 } 119 if (addTrailingSpace(gotSeparateLine)) { 120 toInsert = toInsert + " "; 121 } 122 } 123 return toInsert; 124 } 125 126 /** 127 * Gets the insertion text. 128 * 129 * @param comments 130 * if true, Java 8 features will be surrounded in comments 131 * @param abbreviate 132 * if true, the package name will be removed from the annotations. 133 * The package name can be retrieved again by calling the 134 * {@link #getPackageNames()} method. 135 * @return the text to insert 136 */ 137 protected abstract String getText(boolean comments, boolean abbreviate); 138 139 /** 140 * Indicates if a preceding space should be added to this insertion. 141 * Subclasses may override this method for custom leading space rules. 142 * 143 * @param gotSeparateLine 144 * {@code true} if this insertion is actually added on a separate 145 * line. 146 * @param pos 147 * the source position where this insertion will be inserted 148 * @param precedingChar 149 * the character directly preceding where this insertion will be 150 * inserted. This value will be ignored if {@code pos} is 0. 151 * @return {@code true} if a leading space should be added, {@code false} 152 * otherwise. 153 */ 154 protected boolean addLeadingSpace(boolean gotSeparateLine, int pos, 155 char precedingChar) { 156 // Don't add a preceding space if this insertion is on its own line, 157 // it's at the beginning of the file, the preceding character is already 158 // whitespace, or the preceding character is the first formal or generic 159 // parameter. 160 return !gotSeparateLine && pos != 0 161 && !Character.isWhitespace(precedingChar) 162 && precedingChar != '(' && precedingChar != '<'; 163 } 164 165 /** 166 * Indicates if a trailing space should be added to this insertion. 167 * Subclasses may override this method for custom trailing space rules. 168 * 169 * @param gotSeparateLine 170 * {@code true} if this insertion is actually added on a separate 171 * line. 172 * @return {@code} true if a trailing space should be added, {@code false} 173 * otherwise. 174 */ 175 protected boolean addTrailingSpace(boolean gotSeparateLine) { 176 // Don't added a trailing space if this insertion is on its own line. 177 return !gotSeparateLine; 178 } 179 180 /** 181 * Gets the package name. 182 * 183 * @return the package name of the annotation being inserted by this 184 * Insertion. This will be empty unless 185 * {@link #getText(boolean, boolean)} is called with abbreviate true. 186 */ 187 public Set<String> getPackageNames() { 188 return packageNames; 189 } 190 191 /** 192 * Gets the set of annotation names that should always be qualified. 193 */ 194 public static Set<String> getAlwaysQualify() { 195 return alwaysQualify; 196 } 197 198 /** 199 * Sets the set of annotation names that should always be qualified. 200 */ 201 public static void setAlwaysQualify(Set<String> set) { 202 alwaysQualify = set; 203 } 204 205 /** 206 * Gets whether the insertion goes on a separate line. 207 * 208 * @return whether the insertion goes on a separate line 209 */ 210 public boolean getSeparateLine() { 211 return separateLine; 212 } 213 214 /** 215 * Gets whether this insertion has already been inserted into source code. 216 * @return {@code true} if this insertion has already been inserted, 217 * {@code false} otherwise. 218 */ 219 public boolean getInserted() { 220 return inserted; 221 } 222 223 /** 224 * Sets whether this insertion has already been inserted into source code. 225 * @param inserted {@code true} if this insertion has already been inserted, 226 * {@code false} otherwise. 227 */ 228 public void setInserted(boolean inserted) { 229 this.inserted = inserted; 230 } 231 232 /** 233 * {@inheritDoc} 234 */ 235 @Override 236 public String toString() { 237 return String.format("(nl=%b) @ %s", separateLine, criteria); 238 } 239 240 /** 241 * Gets the kind of this insertion. 242 */ 243 public abstract Kind getKind(); 244 245 /** 246 * Removes the leading package. 247 * 248 * @return given <code>@com.foo.bar(baz)</code> it returns the pair 249 * <code>{ com.foo, @bar(baz) }</code>. 250 */ 251 public static Pair<String, String> removePackage(String s) { 252 int nameEnd = s.indexOf("("); 253 if (nameEnd == -1) { 254 nameEnd = s.length(); 255 } 256 int dotIndex = s.lastIndexOf(".", nameEnd); 257 if (dotIndex != -1) { 258 String basename = s.substring(dotIndex + 1); 259 if (!alwaysQualify.contains(basename)) { 260 String packageName = s.substring(0, nameEnd); 261 if (packageName.startsWith("@")) { 262 return Pair.of(packageName.substring(1), 263 "@" + basename); 264 } else { 265 return Pair.of(packageName, basename); 266 } 267 } 268 } 269 return Pair.of((String) null, s); 270 } 271 272 /** 273 * Converts the given type to a String. This method can't be in the 274 * {@link Type} class because this method relies on the {@link Insertion} 275 * class to format annotations, and the {@link Insertion} class is not 276 * available from {@link Type}. 277 * 278 * @param type 279 * the type to convert 280 * @param comments 281 * if true, Java 8 features will be surrounded in comments 282 * @param abbreviate 283 * if true, the package name will be removed from the annotations. 284 * The package name can be retrieved again by calling the 285 * {@link #getPackageNames()} method. 286 * @return the type as a string 287 */ 288 public String typeToString(Type type, boolean comments, boolean abbreviate) { 289 StringBuilder result = new StringBuilder(); 290 291 switch (type.getKind()) { 292 case DECLARED: 293 DeclaredType declaredType = (DeclaredType) type; 294 String typeName = declaredType.getName(); 295 int sep = typeName.lastIndexOf('.') + 1; 296 if (abbreviate) { 297 typeName = typeName.substring(sep); 298 } else if (sep > 0) { 299 result.append(typeName.substring(0, sep)); 300 typeName = typeName.substring(sep); 301 } 302 writeAnnotations(type, result, comments, abbreviate); 303 result.append(typeName); 304 if (!declaredType.isWildcard()) { 305 List<Type> typeArguments = declaredType.getTypeParameters(); 306 if (!typeArguments.isEmpty()) { 307 result.append('<'); 308 result.append(typeToString(typeArguments.get(0), comments, abbreviate)); 309 for (int i = 1; i < typeArguments.size(); i++) { 310 result.append(", "); 311 result.append(typeToString(typeArguments.get(i), comments, abbreviate)); 312 } 313 result.append('>'); 314 } 315 Type innerType = declaredType.getInnerType(); 316 if (innerType != null) { 317 result.append('.'); 318 result.append(typeToString(innerType, comments, abbreviate)); 319 } 320 } 321 break; 322 case ARRAY: 323 ArrayType arrayType = (ArrayType) type; 324 result.append(typeToString(arrayType.getComponentType(), comments, abbreviate)); 325 if (!arrayType.getAnnotations().isEmpty()) { 326 result.append(' '); 327 } 328 writeAnnotations(type, result, comments, abbreviate); 329 result.append("[]"); 330 break; 331 case BOUNDED: 332 BoundedType boundedType = (BoundedType) type; 333 result.append(typeToString(boundedType.getName(), comments, abbreviate)); 334 result.append(' '); 335 result.append(boundedType.getBoundKind()); 336 result.append(' '); 337 result.append(typeToString(boundedType.getBound(), comments, abbreviate)); 338 break; 339 default: 340 throw new RuntimeException("Illegal kind: " + type.getKind()); 341 } 342 // There will be extra whitespace at the end if this is only annotations, so trim 343 return result.toString().trim(); 344 } 345 346 /** 347 * Writes the annotations on the given type to the given 348 * {@link StringBuilder}. 349 * 350 * @param type 351 * contains the annotations to write. Only the annotations 352 * directly on the type will be written. Subtypes will be 353 * ignored. 354 * @param result 355 * where to write the annotations 356 * @param comments 357 * if {@code true}, Java 8 features will be surrounded in 358 * comments. 359 * @param abbreviate 360 * if {@code true}, the package name will be removed from the 361 * annotations. The package name can be retrieved again by 362 * calling the {@link #getPackageNames()} method. 363 */ 364 private void writeAnnotations(Type type, StringBuilder result, 365 boolean comments, boolean abbreviate) { 366 for (String annotation : type.getAnnotations()) { 367 AnnotationInsertion ins = new AnnotationInsertion(annotation); 368 result.append(ins.getText(comments, abbreviate)); 369 result.append(" "); 370 if (abbreviate) { 371 packageNames.addAll(ins.getPackageNames()); 372 } 373 } 374 } 375 376 /** 377 * Adds each of the given inner type insertions to the correct part of the 378 * type, based on the insertion's type path. 379 * 380 * @param innerTypeInsertions 381 * the insertions to add to the type. These must be inner type 382 * insertions, meaning each of the insertions' {@link Criteria} 383 * must contain a {@link GenericArrayLocationCriterion} and 384 * {@link GenericArrayLocationCriterion#getLocation()} must return a 385 * non-empty list. 386 * @param outerType the type to add the insertions to 387 */ 388 public static void decorateType(List<Insertion> innerTypeInsertions, final Type outerType) { 389 decorateType(innerTypeInsertions, outerType, null); 390 } 391 392 public static void decorateType(List<Insertion> innerTypeInsertions, 393 final Type outerType, ASTPath outerPath) { 394 for (Insertion innerInsertion : innerTypeInsertions) { 395 // Set each annotation as inserted (even if it doesn't actually get 396 // inserted because of an error) to "disable" the insertion in the global 397 // insertion list. 398 innerInsertion.setInserted(true); 399 400 try { 401 if (innerInsertion.getKind() != Insertion.Kind.ANNOTATION) { 402 throw new RuntimeException("Expected 'ANNOTATION' insertion kind, got '" 403 + innerInsertion.getKind() + "'."); 404 } 405 GenericArrayLocationCriterion c = innerInsertion.getCriteria().getGenericArrayLocation(); 406 String annos = 407 ((AnnotationInsertion) innerInsertion).getAnnotation(); 408 if (c == null) { 409 ASTPath astPath = innerInsertion.getCriteria().getASTPath(); 410 if (outerPath != null && astPath != null) { 411 decorateType(astPath, annos, outerType, outerPath); 412 continue; 413 } 414 throw new RuntimeException("Missing type path."); 415 } 416 417 List<TypePathEntry> location = c.getLocation(); 418 Type type = outerType; 419 420 // Use the type path entries to traverse through the type. Throw an 421 // exception and move on to the next inner type insertion if the type 422 // path and actual type don't match up. 423 for (TypePathEntry tpe : location) { 424 switch (tpe.tag) { 425 case ARRAY: 426 if (type.getKind() == Type.Kind.ARRAY) { 427 type = ((ArrayType) type).getComponentType(); 428 } else { 429 throw new RuntimeException("Incorrect type path."); 430 } 431 break; 432 case INNER_TYPE: 433 if (type.getKind() == Type.Kind.DECLARED) { 434 DeclaredType declaredType = (DeclaredType) type; 435 if (declaredType.getInnerType() == null) { 436 throw new RuntimeException("Incorrect type path: " 437 + "expected inner type but none exists."); 438 } 439 type = declaredType.getInnerType(); 440 } else { 441 throw new RuntimeException("Incorrect type path."); 442 } 443 break; 444 case WILDCARD: 445 if (type.getKind() == Type.Kind.BOUNDED) { 446 BoundedType boundedType = (BoundedType) type; 447 if (boundedType.getBound() == null) { 448 throw new RuntimeException("Incorrect type path: " 449 + "expected type bound but none exists."); 450 } 451 type = boundedType.getBound(); 452 } else { 453 throw new RuntimeException("Incorrect type path."); 454 } 455 break; 456 case TYPE_ARGUMENT: 457 if (type.getKind() == Type.Kind.DECLARED) { 458 DeclaredType declaredType = (DeclaredType) type; 459 if (0 <= tpe.arg && tpe.arg < 460 declaredType.getTypeParameters().size()) { 461 type = declaredType.getTypeParameter(tpe.arg); 462 } else { 463 throw new RuntimeException("Incorrect type argument index: " + tpe.arg); 464 } 465 } else { 466 throw new RuntimeException("Incorrect type path."); 467 } 468 break; 469 default: 470 throw new RuntimeException("Illegal TypePathEntryKind: " + tpe.tag); 471 } 472 } 473 if (type.getKind() == Type.Kind.BOUNDED) { 474 // Annotations aren't allowed directly on the BoundedType, see BoundedType 475 type = ((BoundedType) type).getName(); 476 } 477 type.addAnnotation(annos); 478 } catch (Throwable e) { 479 TreeFinder.reportInsertionError(innerInsertion, e); 480 } 481 } 482 } 483 484 private static void decorateType(ASTPath astPath, 485 String annos, Type type, ASTPath outerPath) { 486 // type.addAnnotation(annos); // TODO 487 Iterator<ASTPath.ASTEntry> ii = astPath.iterator(); 488 Iterator<ASTPath.ASTEntry> oi = outerPath.iterator(); 489 490 while (oi.hasNext()) { 491 if (!ii.hasNext() || !oi.next().equals(ii.next())) { 492 throw new RuntimeException("Incorrect AST path."); 493 } 494 } 495 496 while (ii.hasNext()) { 497 ASTPath.ASTEntry entry = ii.next(); 498 Tree.Kind kind = entry.getTreeKind(); 499 switch (kind) { 500 case ARRAY_TYPE: 501 if (type.getKind() == Type.Kind.ARRAY) { 502 type = ((ArrayType) type).getComponentType(); 503 } else { 504 throw new RuntimeException("Incorrect type path."); 505 } 506 break; 507 case MEMBER_SELECT: 508 if (type.getKind() == Type.Kind.DECLARED) { 509 DeclaredType declaredType = (DeclaredType) type; 510 if (declaredType.getInnerType() == null) { 511 throw new RuntimeException("Incorrect type path: " 512 + "expected inner type but none exists."); 513 } 514 type = declaredType.getInnerType(); 515 } else { 516 throw new RuntimeException("Incorrect type path."); 517 } 518 break; 519 case PARAMETERIZED_TYPE: 520 if (type.getKind() == Type.Kind.DECLARED) { 521 int arg = entry.getArgument(); 522 DeclaredType declaredType = (DeclaredType) type; 523 if (0 <= arg && arg < declaredType.getTypeParameters().size()) { 524 type = declaredType.getTypeParameter(arg); 525 } else { 526 throw new RuntimeException("Incorrect type argument index: " + arg); 527 } 528 } else { 529 throw new RuntimeException("Incorrect type path."); 530 } 531 break; 532 case UNBOUNDED_WILDCARD: 533 if (type.getKind() == Type.Kind.BOUNDED) { 534 BoundedType boundedType = (BoundedType) type; 535 if (boundedType.getBound() == null) { 536 throw new RuntimeException("Incorrect type path: " 537 + "expected type bound but none exists."); 538 } 539 type = boundedType.getBound(); 540 } else { 541 throw new RuntimeException("Incorrect type path."); 542 } 543 break; 544 default: 545 throw new RuntimeException("Illegal TreeKind: " + kind); 546 } 547 } 548 if (type.getKind() == Type.Kind.BOUNDED) { 549 // Annotations aren't allowed directly on the BoundedType, see BoundedType 550 type = ((BoundedType) type).getName(); 551 } 552 type.addAnnotation(annos); 553 } 554 } 555