Home | History | Annotate | Download | only in find
      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