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