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