Home | History | Annotate | Download | only in extractstring
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
      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 
     17 package com.android.ide.eclipse.adt.internal.refactorings.extractstring;
     18 
     19 import org.eclipse.jdt.core.dom.AST;
     20 import org.eclipse.jdt.core.dom.ASTNode;
     21 import org.eclipse.jdt.core.dom.ASTVisitor;
     22 import org.eclipse.jdt.core.dom.Assignment;
     23 import org.eclipse.jdt.core.dom.ClassInstanceCreation;
     24 import org.eclipse.jdt.core.dom.Expression;
     25 import org.eclipse.jdt.core.dom.IMethodBinding;
     26 import org.eclipse.jdt.core.dom.ITypeBinding;
     27 import org.eclipse.jdt.core.dom.IVariableBinding;
     28 import org.eclipse.jdt.core.dom.MethodDeclaration;
     29 import org.eclipse.jdt.core.dom.MethodInvocation;
     30 import org.eclipse.jdt.core.dom.Modifier;
     31 import org.eclipse.jdt.core.dom.Name;
     32 import org.eclipse.jdt.core.dom.SimpleName;
     33 import org.eclipse.jdt.core.dom.SimpleType;
     34 import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
     35 import org.eclipse.jdt.core.dom.StringLiteral;
     36 import org.eclipse.jdt.core.dom.Type;
     37 import org.eclipse.jdt.core.dom.TypeDeclaration;
     38 import org.eclipse.jdt.core.dom.VariableDeclarationExpression;
     39 import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
     40 import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
     41 import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
     42 import org.eclipse.text.edits.TextEditGroup;
     43 
     44 import java.util.ArrayList;
     45 import java.util.List;
     46 import java.util.TreeMap;
     47 
     48 /**
     49  * Visitor used by {@link ExtractStringRefactoring} to extract a string from an existing
     50  * Java source and replace it by an Android XML string reference.
     51  *
     52  * @see ExtractStringRefactoring#computeJavaChanges
     53  */
     54 class ReplaceStringsVisitor extends ASTVisitor {
     55 
     56     private static final String CLASS_ANDROID_CONTEXT    = "android.content.Context"; //$NON-NLS-1$
     57     private static final String CLASS_JAVA_CHAR_SEQUENCE = "java.lang.CharSequence";  //$NON-NLS-1$
     58     private static final String CLASS_JAVA_STRING        = "java.lang.String";        //$NON-NLS-1$
     59 
     60 
     61     private final AST mAst;
     62     private final ASTRewrite mRewriter;
     63     private final String mOldString;
     64     private final String mRQualifier;
     65     private final String mXmlId;
     66     private final ArrayList<TextEditGroup> mEditGroups;
     67 
     68     public ReplaceStringsVisitor(AST ast,
     69             ASTRewrite astRewrite,
     70             ArrayList<TextEditGroup> editGroups,
     71             String oldString,
     72             String rQualifier,
     73             String xmlId) {
     74         mAst = ast;
     75         mRewriter = astRewrite;
     76         mEditGroups = editGroups;
     77         mOldString = oldString;
     78         mRQualifier = rQualifier;
     79         mXmlId = xmlId;
     80     }
     81 
     82     @SuppressWarnings("unchecked")
     83     @Override
     84     public boolean visit(StringLiteral node) {
     85         if (node.getLiteralValue().equals(mOldString)) {
     86 
     87             // We want to analyze the calling context to understand whether we can
     88             // just replace the string literal by the named int constant (R.id.foo)
     89             // or if we should generate a Context.getString() call.
     90             boolean useGetResource = false;
     91             useGetResource = examineVariableDeclaration(node) ||
     92                                 examineMethodInvocation(node) ||
     93                                 examineAssignment(node);
     94 
     95             Name qualifierName = mAst.newName(mRQualifier + ".string");     //$NON-NLS-1$
     96             SimpleName idName = mAst.newSimpleName(mXmlId);
     97             ASTNode newNode = mAst.newQualifiedName(qualifierName, idName);
     98             boolean disabledChange = false;
     99             String title = "Replace string by ID";
    100 
    101             if (useGetResource) {
    102                 Expression context = methodHasContextArgument(node);
    103                 if (context == null && !isClassDerivedFromContext(node)) {
    104                     // if we don't have a class that derives from Context and
    105                     // we don't have a Context method argument, then try a bit harder:
    106                     // can we find a method or a field that will give us a context?
    107                     context = findContextFieldOrMethod(node);
    108 
    109                     if (context == null) {
    110                         // If not, let's  write Context.getString(), which is technically
    111                         // invalid but makes it a good clue on how to fix it. Since these
    112                         // will not compile, we create a disabled change by default.
    113                         context = mAst.newSimpleName("Context");            //$NON-NLS-1$
    114                         disabledChange = true;
    115                     }
    116                 }
    117 
    118                 MethodInvocation mi2 = mAst.newMethodInvocation();
    119                 mi2.setName(mAst.newSimpleName("getString"));               //$NON-NLS-1$
    120                 mi2.setExpression(context);
    121                 mi2.arguments().add(newNode);
    122 
    123                 newNode = mi2;
    124                 title = "Replace string by Context.getString(R.string...)";
    125             }
    126 
    127             TextEditGroup editGroup = new EnabledTextEditGroup(title, !disabledChange);
    128             mEditGroups.add(editGroup);
    129             mRewriter.replace(node, newNode, editGroup);
    130         }
    131         return super.visit(node);
    132     }
    133 
    134     /**
    135      * Examines if the StringLiteral is part of an assignment corresponding to the
    136      * a string variable declaration, e.g. String foo = id.
    137      *
    138      * The parent fragment is of syntax "var = expr" or "var[] = expr".
    139      * We want the type of the variable, which is either held by a
    140      * VariableDeclarationStatement ("type [fragment]") or by a
    141      * VariableDeclarationExpression. In either case, the type can be an array
    142      * but for us all that matters is to know whether the type is an int or
    143      * a string.
    144      */
    145     private boolean examineVariableDeclaration(StringLiteral node) {
    146         VariableDeclarationFragment fragment = findParentClass(node,
    147                 VariableDeclarationFragment.class);
    148 
    149         if (fragment != null) {
    150             ASTNode parent = fragment.getParent();
    151 
    152             Type type = null;
    153             if (parent instanceof VariableDeclarationStatement) {
    154                 type = ((VariableDeclarationStatement) parent).getType();
    155             } else if (parent instanceof VariableDeclarationExpression) {
    156                 type = ((VariableDeclarationExpression) parent).getType();
    157             }
    158 
    159             if (type instanceof SimpleType) {
    160                 return isJavaString(type.resolveBinding());
    161             }
    162         }
    163 
    164         return false;
    165     }
    166 
    167     /**
    168      * Examines if the StringLiteral is part of a assignment to a variable that
    169      * is a string. We need to lookup the variable to find its type, either in the
    170      * enclosing method or class type.
    171      */
    172     private boolean examineAssignment(StringLiteral node) {
    173 
    174         Assignment assignment = findParentClass(node, Assignment.class);
    175         if (assignment != null) {
    176             Expression left = assignment.getLeftHandSide();
    177 
    178             ITypeBinding typeBinding = left.resolveTypeBinding();
    179             return isJavaString(typeBinding);
    180         }
    181 
    182         return false;
    183     }
    184 
    185     /**
    186      * If the expression is part of a method invocation (aka a function call) or a
    187      * class instance creation (aka a "new SomeClass" constructor call), we try to
    188      * find the type of the argument being used. If it is a String (most likely), we
    189      * want to return true (to generate a getString() call). However if there might
    190      * be a similar method that takes an int, in which case we don't want to do that.
    191      *
    192      * This covers the case of Activity.setTitle(int resId) vs setTitle(String str).
    193      */
    194     @SuppressWarnings("rawtypes")
    195     private boolean examineMethodInvocation(StringLiteral node) {
    196 
    197         ASTNode parent = null;
    198         List arguments = null;
    199         IMethodBinding methodBinding = null;
    200 
    201         MethodInvocation invoke = findParentClass(node, MethodInvocation.class);
    202         if (invoke != null) {
    203             parent = invoke;
    204             arguments = invoke.arguments();
    205             methodBinding = invoke.resolveMethodBinding();
    206         } else {
    207             ClassInstanceCreation newclass = findParentClass(node, ClassInstanceCreation.class);
    208             if (newclass != null) {
    209                 parent = newclass;
    210                 arguments = newclass.arguments();
    211                 methodBinding = newclass.resolveConstructorBinding();
    212             }
    213         }
    214 
    215         if (parent != null && arguments != null && methodBinding != null) {
    216             // We want to know which argument this is.
    217             // Walk up the hierarchy again to find the immediate child of the parent,
    218             // which should turn out to be one of the invocation arguments.
    219             ASTNode child = null;
    220             for (ASTNode n = node; n != parent; ) {
    221                 ASTNode p = n.getParent();
    222                 if (p == parent) {
    223                     child = n;
    224                     break;
    225                 }
    226                 n = p;
    227             }
    228             if (child == null) {
    229                 // This can't happen: a parent of 'node' must be the child of 'parent'.
    230                 return false;
    231             }
    232 
    233             // Find the index
    234             int index = 0;
    235             for (Object arg : arguments) {
    236                 if (arg == child) {
    237                     break;
    238                 }
    239                 index++;
    240             }
    241 
    242             if (index == arguments.size()) {
    243                 // This can't happen: one of the arguments of 'invoke' must be 'child'.
    244                 return false;
    245             }
    246 
    247             // Eventually we want to determine if the parameter is a string type,
    248             // in which case a Context.getString() call must be generated.
    249             boolean useStringType = false;
    250 
    251             // Find the type of that argument
    252             ITypeBinding[] types = methodBinding.getParameterTypes();
    253             if (index < types.length) {
    254                 ITypeBinding type = types[index];
    255                 useStringType = isJavaString(type);
    256             }
    257 
    258             // Now that we know that this method takes a String parameter, can we find
    259             // a variant that would accept an int for the same parameter position?
    260             if (useStringType) {
    261                 String name = methodBinding.getName();
    262                 ITypeBinding clazz = methodBinding.getDeclaringClass();
    263                 nextMethod: for (IMethodBinding mb2 : clazz.getDeclaredMethods()) {
    264                     if (methodBinding == mb2 || !mb2.getName().equals(name)) {
    265                         continue;
    266                     }
    267                     // We found a method with the same name. We want the same parameters
    268                     // except that the one at 'index' must be an int type.
    269                     ITypeBinding[] types2 = mb2.getParameterTypes();
    270                     int len2 = types2.length;
    271                     if (types.length == len2) {
    272                         for (int i = 0; i < len2; i++) {
    273                             if (i == index) {
    274                                 ITypeBinding type2 = types2[i];
    275                                 if (!("int".equals(type2.getQualifiedName()))) {   //$NON-NLS-1$
    276                                     // The argument at 'index' is not an int.
    277                                     continue nextMethod;
    278                                 }
    279                             } else if (!types[i].equals(types2[i])) {
    280                                 // One of the other arguments do not match our original method
    281                                 continue nextMethod;
    282                             }
    283                         }
    284                         // If we got here, we found a perfect match: a method with the same
    285                         // arguments except the one at 'index' is an int. In this case we
    286                         // don't need to convert our R.id into a string.
    287                         useStringType = false;
    288                         break;
    289                     }
    290                 }
    291             }
    292 
    293             return useStringType;
    294         }
    295         return false;
    296     }
    297 
    298     /**
    299      * Examines if the StringLiteral is part of a method declaration (a.k.a. a function
    300      * definition) which takes a Context argument.
    301      * If such, it returns the name of the variable as a {@link SimpleName}.
    302      * Otherwise it returns null.
    303      */
    304     private SimpleName methodHasContextArgument(StringLiteral node) {
    305         MethodDeclaration decl = findParentClass(node, MethodDeclaration.class);
    306         if (decl != null) {
    307             for (Object obj : decl.parameters()) {
    308                 if (obj instanceof SingleVariableDeclaration) {
    309                     SingleVariableDeclaration var = (SingleVariableDeclaration) obj;
    310                     if (isAndroidContext(var.getType())) {
    311                         return mAst.newSimpleName(var.getName().getIdentifier());
    312                     }
    313                 }
    314             }
    315         }
    316         return null;
    317     }
    318 
    319     /**
    320      * Walks up the node hierarchy to find the class (aka type) where this statement
    321      * is used and returns true if this class derives from android.content.Context.
    322      */
    323     private boolean isClassDerivedFromContext(StringLiteral node) {
    324         TypeDeclaration clazz = findParentClass(node, TypeDeclaration.class);
    325         if (clazz != null) {
    326             // This is the class that the user is currently writing, so it can't be
    327             // a Context by itself, it has to be derived from it.
    328             return isAndroidContext(clazz.getSuperclassType());
    329         }
    330         return false;
    331     }
    332 
    333     private Expression findContextFieldOrMethod(StringLiteral node) {
    334         TypeDeclaration clazz = findParentClass(node, TypeDeclaration.class);
    335         return clazz == null ? null : findContextFieldOrMethod(clazz.resolveBinding());
    336     }
    337 
    338     private Expression findContextFieldOrMethod(ITypeBinding clazzType) {
    339         TreeMap<Integer, Expression> results = new TreeMap<Integer, Expression>();
    340         findContextCandidates(results, clazzType, 0 /*superType*/);
    341         if (results.size() > 0) {
    342             Integer bestRating = results.keySet().iterator().next();
    343             return results.get(bestRating);
    344         }
    345         return null;
    346     }
    347 
    348     /**
    349      * Find all method or fields that are candidates for providing a Context.
    350      * There can be various choices amongst this class or its super classes.
    351      * Sort them by rating in the results map.
    352      *
    353      * The best ever choice is to find a method with no argument that returns a Context.
    354      * The second suitable choice is to find a Context field.
    355      * The least desirable choice is to find a method with arguments. It's not really
    356      * desirable since we can't generate these arguments automatically.
    357      *
    358      * Methods and fields from supertypes are ignored if they are private.
    359      *
    360      * The rating is reversed: the lowest rating integer is used for the best candidate.
    361      * Because the superType argument is actually a recursion index, this makes the most
    362      * immediate classes more desirable.
    363      *
    364      * @param results The map that accumulates the rating=>expression results. The lower
    365      *                rating number is the best candidate.
    366      * @param clazzType The class examined.
    367      * @param superType The recursion index.
    368      *                  0 for the immediate class, 1 for its super class, etc.
    369      */
    370     private void findContextCandidates(TreeMap<Integer, Expression> results,
    371             ITypeBinding clazzType,
    372             int superType) {
    373         for (IMethodBinding mb : clazzType.getDeclaredMethods()) {
    374             // If we're looking at supertypes, we can't use private methods.
    375             if (superType != 0 && Modifier.isPrivate(mb.getModifiers())) {
    376                 continue;
    377             }
    378 
    379             if (isAndroidContext(mb.getReturnType())) {
    380                 // We found a method that returns something derived from Context.
    381 
    382                 int argsLen = mb.getParameterTypes().length;
    383                 if (argsLen == 0) {
    384                     // We'll favor any method that takes no argument,
    385                     // That would be the best candidate ever, so we can stop here.
    386                     MethodInvocation mi = mAst.newMethodInvocation();
    387                     mi.setName(mAst.newSimpleName(mb.getName()));
    388                     results.put(Integer.MIN_VALUE, mi);
    389                     return;
    390                 } else {
    391                     // A method with arguments isn't as interesting since we wouldn't
    392                     // know how to populate such arguments. We'll use it if there are
    393                     // no other alternatives. We'll favor the one with the less arguments.
    394                     Integer rating = Integer.valueOf(10000 + 1000 * superType + argsLen);
    395                     if (!results.containsKey(rating)) {
    396                         MethodInvocation mi = mAst.newMethodInvocation();
    397                         mi.setName(mAst.newSimpleName(mb.getName()));
    398                         results.put(rating, mi);
    399                     }
    400                 }
    401             }
    402         }
    403 
    404         // A direct Context field would be more interesting than a method with
    405         // arguments. Try to find one.
    406         for (IVariableBinding var : clazzType.getDeclaredFields()) {
    407             // If we're looking at supertypes, we can't use private field.
    408             if (superType != 0 && Modifier.isPrivate(var.getModifiers())) {
    409                 continue;
    410             }
    411 
    412             if (isAndroidContext(var.getType())) {
    413                 // We found such a field. Let's use it.
    414                 Integer rating = Integer.valueOf(superType);
    415                 results.put(rating, mAst.newSimpleName(var.getName()));
    416                 break;
    417             }
    418         }
    419 
    420         // Examine the super class to see if we can locate a better match
    421         clazzType = clazzType.getSuperclass();
    422         if (clazzType != null) {
    423             findContextCandidates(results, clazzType, superType + 1);
    424         }
    425     }
    426 
    427     /**
    428      * Walks up the node hierarchy and returns the first ASTNode of the requested class.
    429      * Only look at parents.
    430      *
    431      * Implementation note: this is a generic method so that it returns the node already
    432      * casted to the requested type.
    433      */
    434     @SuppressWarnings("unchecked")
    435     private <T extends ASTNode> T findParentClass(ASTNode node, Class<T> clazz) {
    436         if (node != null) {
    437             for (node = node.getParent(); node != null; node = node.getParent()) {
    438                 if (node.getClass().equals(clazz)) {
    439                     return (T) node;
    440                 }
    441             }
    442         }
    443         return null;
    444     }
    445 
    446     /**
    447      * Returns true if the given type is or derives from android.content.Context.
    448      */
    449     private boolean isAndroidContext(Type type) {
    450         if (type != null) {
    451             return isAndroidContext(type.resolveBinding());
    452         }
    453         return false;
    454     }
    455 
    456     /**
    457      * Returns true if the given type is or derives from android.content.Context.
    458      */
    459     private boolean isAndroidContext(ITypeBinding type) {
    460         for (; type != null; type = type.getSuperclass()) {
    461             if (CLASS_ANDROID_CONTEXT.equals(type.getQualifiedName())) {
    462                 return true;
    463             }
    464         }
    465         return false;
    466     }
    467 
    468     /**
    469      * Returns true if this type binding represents a String or CharSequence type.
    470      */
    471     private boolean isJavaString(ITypeBinding type) {
    472         for (; type != null; type = type.getSuperclass()) {
    473             if (CLASS_JAVA_STRING.equals(type.getQualifiedName()) ||
    474                 CLASS_JAVA_CHAR_SEQUENCE.equals(type.getQualifiedName())) {
    475                 return true;
    476             }
    477         }
    478         return false;
    479     }
    480 }
    481