2 * Copyright (C) 2009 The Android Open Source Project
\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
8 * http://www.eclipse.org/org/documents/epl-v10.php
\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
17 package com.android.ide.eclipse.adt.internal.refactorings.extractstring;
\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
43 import java.util.ArrayList;
\r
44 import java.util.List;
\r
45 import java.util.TreeMap;
\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
51 * @see ExtractStringRefactoring#computeJavaChanges
\r
53 class ReplaceStringsVisitor extends ASTVisitor {
\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
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
67 public ReplaceStringsVisitor(AST ast,
\r
68 ASTRewrite astRewrite,
\r
69 ArrayList<TextEditGroup> editGroups,
\r
74 mRewriter = astRewrite;
\r
75 mEditGroups = editGroups;
\r
76 mOldString = oldString;
\r
77 mRQualifier = rQualifier;
\r
81 @SuppressWarnings("unchecked") //$NON-NLS-1$
\r
83 public boolean visit(StringLiteral node) {
\r
84 if (node.getLiteralValue().equals(mOldString)) {
\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
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
98 if (useGetResource) {
\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
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
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
120 title = "Replace string by Context.getString(R.string...)";
\r
123 TextEditGroup editGroup = new TextEditGroup(title);
\r
124 mEditGroups.add(editGroup);
\r
125 mRewriter.replace(node, newNode, editGroup);
\r
127 return super.visit(node);
\r
131 * Examines if the StringLiteral is part of of an assignment to a string,
\r
132 * e.g. String foo = id.
\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
141 private boolean examineVariableDeclaration(StringLiteral node) {
\r
142 VariableDeclarationFragment fragment = findParentClass(node,
\r
143 VariableDeclarationFragment.class);
\r
145 if (fragment != null) {
\r
146 ASTNode parent = fragment.getParent();
\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
155 if (type instanceof SimpleType) {
\r
156 return isJavaString(type.resolveBinding());
\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
170 * This covers the case of Activity.setTitle(int resId) vs setTitle(String str).
\r
172 @SuppressWarnings("unchecked") //$NON-NLS-1$
\r
173 private boolean examineMethodInvocation(StringLiteral node) {
\r
175 ASTNode parent = null;
\r
176 List arguments = null;
\r
177 IMethodBinding methodBinding = null;
\r
179 MethodInvocation invoke = findParentClass(node, MethodInvocation.class);
\r
180 if (invoke != null) {
\r
182 arguments = invoke.arguments();
\r
183 methodBinding = invoke.resolveMethodBinding();
\r
185 ClassInstanceCreation newclass = findParentClass(node, ClassInstanceCreation.class);
\r
186 if (newclass != null) {
\r
188 arguments = newclass.arguments();
\r
189 methodBinding = newclass.resolveConstructorBinding();
\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
206 if (child == null) {
\r
207 // This can't happen: a parent of 'node' must be the child of 'parent'.
\r
213 for (Object arg : arguments) {
\r
214 if (arg == child) {
\r
220 if (index == arguments.size()) {
\r
221 // This can't happen: one of the arguments of 'invoke' must be 'child'.
\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
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
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
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
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
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
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
271 return useStringType;
\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
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
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
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
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
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
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
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
337 * Methods and fields from supertypes are ignored if they are private.
\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
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
349 private void findContextCandidates(TreeMap<Integer, Expression> results,
\r
350 ITypeBinding clazzType,
\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
358 if (isAndroidContext(mb.getReturnType())) {
\r
359 // We found a method that returns something derived from Context.
\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
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
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
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
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
407 * Walks up the node hierarchy and returns the first ASTNode of the requested class.
\r
408 * Only look at parents.
\r
410 * Implementation note: this is a generic method so that it returns the node already
\r
411 * casted to the requested type.
\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
424 * Returns true if the given type is or derives from android.content.Context.
\r
426 private boolean isAndroidContext(Type type) {
\r
427 if (type != null) {
\r
428 return isAndroidContext(type.resolveBinding());
\r
434 * Returns true if the given type is or derives from android.content.Context.
\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
446 * Returns true if this type binding represents a String or CharSequence type.
\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