OSDN Git Service

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