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 
ReplaceStringsVisitor(AST ast, ASTRewrite astRewrite, ArrayList<TextEditGroup> editGroups, String oldString, String rQualifier, String xmlId)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
visit(StringLiteral node)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      */
examineVariableDeclaration(StringLiteral node)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      */
examineAssignment(StringLiteral node)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")
examineMethodInvocation(StringLiteral node)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      */
methodHasContextArgument(StringLiteral node)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      */
isClassDerivedFromContext(StringLiteral node)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 
findContextFieldOrMethod(StringLiteral node)333     private Expression findContextFieldOrMethod(StringLiteral node) {
334         TypeDeclaration clazz = findParentClass(node, TypeDeclaration.class);
335         return clazz == null ? null : findContextFieldOrMethod(clazz.resolveBinding());
336     }
337 
findContextFieldOrMethod(ITypeBinding clazzType)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      */
findContextCandidates(TreeMap<Integer, Expression> results, ITypeBinding clazzType, int superType)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")
findParentClass(ASTNode node, Class<T> clazz)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      */
isAndroidContext(Type type)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      */
isAndroidContext(ITypeBinding type)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      */
isJavaString(ITypeBinding type)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